Skip to content

T099 release step: version 0.1.0 (plan 034) - #79

Merged
brettheap merged 4 commits into
mainfrom
build/034-p3w-t099-release-step-0.1.0
Oct 5, 2026
Merged

brettheap merged 4 commits into
mainfrom
build/034-p3w-t099-release-step-0.1.0

Conversation

@brettheap

@brettheap brettheap commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

Arc: neutral-product-standalone-operability

T099's release step: pyproject.toml's version goes from 0.0.0 to 0.1.0. This is RULED 5963162921 (Brett Heap, 2026-10-02): "0.1.0 (Recommended)". openDox's first public PyPI release (T099, RULED 5962754358 item 1) carries version 0.1.0, as the first public release, with the API still pre-1.0.

It is the last phase-3 openDox-code landing. The order, as 5963162921 sets it:

  1. Every other phase-3 openDox-code landing has landed, including openDox-code#78 (T099's release workflow), T072, 13.1: the bundled PostgreSQL server, the local install's own child (plan 034) #69 (T072) and T075, 10.2 and 10.2a — an installed entry point serves all 42 bundle files; intent-feed.js is not owed (plan 034) #73 (T075).
  2. This PR lands last.
  3. The openDox root pins this commit (T087).
  4. At the cut, after T089, release.yml is dispatched with version = 0.1.0. It publishes only the commit the root's contracts/code-pin.yaml names, and refuses any other.

Ready for the holder, after the final main merge. The title's DRAFT prefix is dropped. The holder posts READY, and the landers merge this PR after openDox-code#78, merging main into it again first if it needs to. The version line stays exactly 0.1.0: T087 pins this PR's landing commit, and T099 publishes that commit.

The final merge of 2026-10-05: main 651c35fe merged, the head is 9519cd8e

The refresh of 2026-10-04: main ca9e1bd5 merged, the head is 31361c91

This PR stays DRAFT, and it lands last. The refresh leaves only a final main merge for later.

🤖 Generated with Claude Code

RULED openxFactory#656 comment 5963162921 (Brett Heap, 2026-10-02):
"0.1.0 (Recommended)". openDox's first public PyPI release carries
version 0.1.0: the first public release, with the API still pre-1.0.

This bump is the last phase-3 openDox-code landing. The openDox root then
pins this commit (T087), and the release workflow (openDox-code#78)
publishes only the commit the root's contracts/code-pin.yaml names, at the
cut, after T089. Nothing else in pyproject.toml moves here; its readme
line rides in #78.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 23:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The focused metadata change is valid and consistent with the documented release plan.

Review effort: Balanced
Findings: None

What changed in this PR

Bumps the opendox package to its first public pre-1.0 release version.

Changes:

  • Sets project version to 0.1.0.
  • Documents the coordinated release and root-pin sequence.
File Description
pyproject.toml Updates package version metadata and release context.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

brettheap and others added 2 commits October 4, 2026 13:12
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 13:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The focused version bump is consistent with the stated release plan and has no unresolved issues.

Review effort: Balanced
Findings: None

…1's version bump to 0.1.0

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 09:53
@sonarqubecloud

sonarqubecloud Bot commented Oct 5, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The valid metadata-only version bump matches the stated release plan and introduces no code issues.

Review effort: Balanced
Findings: None

@brettheap brettheap changed the title DRAFT (phase 3, after T063): T099 release step: version 0.1.0 T099 release step: version 0.1.0 (plan 034) Oct 5, 2026
brettheap added a commit that referenced this pull request Oct 5, 2026
…ine) (plan 034) (#78)

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

Arc: neutral-product-standalone-operability

Plan 034's **T101, "T099's release step"** (with openDox-code#79): a release workflow that publishes openDox to PyPI by trusted publishing (OIDC). It makes release 1's ruled install line, `pip install "opendox[local]"`, work as written (#1144 10.3, as T007's batch H addendum reads, R1Q15 (b) and R1Q16 (iii), RULED `5850003126`). Today `https://pypi.org/pypi/opendox/json` and `https://test.pypi.org/pypi/opendox/json` both answer 404.

- **Ruled**: `5962754358`, item 1 (Brett Heap, 2026-10-02): *"Publish to PyPI at the cut (Recommended)"*. The release commit and version are RULED in `5963162921`, *"0.1.0 (Recommended)"*. The bump to 0.1.0 is the last phase-3 openDox-code landing, T087 pins that commit, and the workflow publishes only the commit the openDox root's `contracts/code-pin.yaml` names.
- **The plan's split** is the holder's ruling, after Copilot's review of openxFactory#1220. **T101** is this PR plus the bump (#79), and lands before T087. **T099** is the publish alone, after T101, T087 and T089. The plan entry is openxFactory#1220 (bookkeeping, no `Arc:` trailer).
- **Ready for the holder, after the final `main` merge.** The title's DRAFT prefix is dropped. The holder posts READY, and the landers merge: this PR first, then openDox-code#79, which merges `main` again first if it needs to. **Nothing has been published** to PyPI or TestPyPI. Nothing in this PR can publish until Brett configures the two indexes (below) and dispatches it on the tag `v0.1.0`. The two environments are configured already, by the holder on 2026-10-05.
- **openDox-code#69 (T072) and #73 (T075) have landed, and this branch merges them** (`main` `8e377823` was merged at `7cc65f93`; the head is now `07cc8f36`: `main` `651c35fe` was merged at `e5f7f554`, then rounds 10, 11 and 12 followed, as the sections below read). The build job passes on this head's own tree (proof 1). Before they landed, the verify step refused, naming exactly what those two PRs add (proof 2).

## What it adds

**`.github/workflows/release.yml`.** Its only trigger is `workflow_dispatch`, with one input, `version`. It has no push, tag or pull-request trigger. Four jobs run in a chain:

1. **`build`** (`contents: read`, `actions: read`; it mints no identity token, and its `GITHUB_TOKEN` reads only). In order:
   - **The release is the pinned commit, on `main`**, in two steps:
     - (a) the ref and `main`: a dispatch on a tag must name `v<version>`, and the run's commit must be on this repository's `main`. The compare API must read `identical` or `ahead`.
     - (b) the pin: the run's commit must equal `commit:` in opensoft/openDox `main`'s `contracts/code-pin.yaml`, whose `source_repository` must be this repository. **Both publish jobs run this same step again, right before their upload** (below).
     - `main`'s head is never required, because T095 may land after T087 has pinned the bump.
     - A dispatch runs at the head of the ref it names. So the release is dispatched on the tag `v<version>`, which the holder creates at the commit T087 pinned, at the cut, on Brett's word. A dispatch on any other tag is refused. A dispatch on `main` or any branch builds and verifies, but cannot deploy: both environments deploy only from the tags `v*` (below).
     - **`main` is always `refs/heads/main`**, in the root's fetch and in the compare. git resolves a bare `main` on the remote as a tag before the branch, and a tag needs no review (round 11).
     - The root's pin is read **anonymously over git**: a depth-1 fetch of opensoft/openDox's `refs/heads/main`, with every credential helper cleared and no prompt, in a repository of its own under `$RUNNER_TEMP`. So the read of another repository depends on no token. The compare reads this repository, through the API.
   - **Both environments ask a reviewer and limit the refs that deploy.** GitHub creates an environment that a job names if it does not exist yet, and creates it with no protection rule. So a dispatch made before Brett set up `pypi` would publish with nobody approving it. And a dispatch runs the dispatched ref's own copy of `release.yml`, while the trusted publisher never matches the ref, so an environment any ref may deploy to would publish whatever a branch carries. The step refuses unless `testpypi` and `pypi` each have a `required_reviewers` rule with at least one reviewer, AND a `deployment_branch_policy` with `custom_branch_policies`, a custom deployment limit (round 11). The limit's pattern, the tag `v*`, is configured and was verified through the API, but the workflow does not re-read it: changing it is an admin act (an accepted limit, RULED `5993462741`).
   - **The release tools come from a hash lock**, `.github/release-tools-cpython312-linux.txt`, installed with `--only-binary :all: --require-hashes` into a venv of their own.
   - **The version gate.** The input must be a PEP 440 version in normalized form, with no epoch (a wheel's file name escapes its `!`) and no local label. It must be a final release. A pre-release or a development release is refused, because the dry run installs the unversioned `opendox[local]`, which pip resolves to a final release once one exists. It must not be `0.0.0`, the scaffold's placeholder, and it must equal `pyproject.toml`'s `[project] version`.
   - **The build.** `python -m build --no-isolation` builds the sdist, then the wheel from that sdist, with `SOURCE_DATE_EPOCH` set to the commit's time.
   - `twine check --strict`.
   - **The artifact checks, before any upload:**
     1. `dist/` holds exactly the sdist and the wheel named for the version;
     2. the wheel's metadata names `opendox` at that version;
     3. the wheel declares exactly the requirements `pyproject.toml` does, for the base install and every extra, and its `local` extra (T072) carries `opendox[runtime]` and `pixeltable-pgserver`. Requirements are compared as normalized requirements: the name, the extras, the version specifier or direct URL, and the marker. Only the `extra == "X"` clause that setuptools adds to record ownership is removed, read from the parsed marker. An `extra` anywhere else in a marker is refused;
     4. the console script `opendox = opendox.cli:main` is declared;
     5. every file git tracks under `src/opendox/web/` is in the wheel, dotfiles included (T075's `web/**/.*`);
     6. every file git tracks under `src/` is in the wheel at its import path, less `src/.gitkeep`, and the wheel carries nothing else but its `.dist-info` and `.data`;
     7. every tracked `migrations/*.sql` is in the wheel's `share/opendox/migrations/` (T072).

     The tracked tree is the reference, so a bundle file added later is checked with no edit here.
   - **The digests are recorded before any third-party code runs.** Right after the artifact checks, the two file names and their sha256 become the job's outputs. Until then only the hash-locked release tools and this package's own build have run (round 10).
   - **The built wheel runs from a fresh venv.** `opendox[local]` is installed from the built file, with dependencies through `constraints-cpython312-linux.txt` and wheels only. Then, from outside the checkout, the step:
     - asserts the import path and the version;
     - walks the whole requirement closure of `opendox[local]` from the installed metadata, extras and markers included, and requires every requirement to be installed and satisfied;
     - runs the bundled server's `initdb --version` and `postgres --version`, through `opendox.runtime.bundle.server_binaries()`;
     - runs `opendox --help`, and requires `opendox generate-and-open --help` to name `--local`.
   - **After the smoke test, `dist/` must still hold exactly those two files at those digests**, so a dependency's code that rewrote them refuses there, by name, before the upload (round 10). Then it uploads `dist/`.
2. **`testpypi`**, the dry run, in the `testpypi` environment, with `id-token: write` and `actions: read`. It downloads `dist/` and checks the two files against the build job's digests, and that there is no third file. **It re-checks its approval at use time**: the environment must still name a required reviewer and still limit its refs (`custom_branch_policies`), and this run's review history must hold an approved review of a deployment to it. **A preflight follows**: TestPyPI must hold no file of this version except a verified one at its verified digest, and none of them yanked (a 404 means no file yet). **It checks the root's pin again**, the build job's step, since the run waited on an approval. Then it runs `pypa/gh-action-pypi-publish` with `repository-url: https://test.pypi.org/legacy/`, bounded at 10 minutes. Every step declares its own timeout, and the job's (25 minutes) covers their sum (24).
3. **`testpypi-install`** (`contents: read`):
   - TestPyPI's JSON API must serve exactly the two verified files and digests, neither yanked (a yanked file refuses at once). It is read until it does, up to 20 reads 15 s apart, so a partial or stale answer during propagation is retried, not taken as final. A read that times out is retried too.
   - Then T099's falsifier runs in a fresh venv: `pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ "opendox[local]"`. The requirement is **unversioned, as written**. The lock, wheels-only (so no package's code runs at install) and `--report` are added.
   - **The installed `opendox` must be the verified TestPyPI wheel, proven before anything from it runs.** pip merges candidates from both indexes, and `opendox` is not reserved on PyPI before its first release. So this job's own `python3`, never the venv's interpreter, reads pip's report. The one `opendox` installed must come from `test-files.pythonhosted.org`, at the version just uploaded, with the verified wheel's sha256.
     - An older `opendox` means a lagging index, and that try is repeated.
     - Any other `opendox` refuses at once, and nothing from it is run: another host, another digest, or a newer version.
     - Each try is a fresh venv, up to 10 tries, 30 s apart, and each is cut off after 300 s. Then `opendox --help`.
   - **Every step declares its own timeout, and the job's (90 minutes) covers their sum**: checkout 5, setup-python 5, the JSON step 16 (20 reads, each up to 30 s, 15 s apart), the install 60 (10 tries, each up to 300 s, 30 s apart). A test reads each bound from the workflow.
   - The trade of using two indexes is stated in the step's comment. The install step receives no token, no OIDC grant and no secret. The job's `GITHUB_TOKEN` (`contents: read`) is used only by its checkout, which does not persist it. The files PyPI receives are still checked by digest.
4. **`pypi`**, in the `pypi` environment, with `id-token: write` and `actions: read`, and a 45-minute timeout that covers its steps' declared bounds (5+2+2+2+3+10+16). The same digest check, **the same approval re-check**, the same preflight against PyPI, **the root's pin checked again** (the run waited on two approvals and on TestPyPI), then `pypa/gh-action-pypi-publish`, bounded at 10 minutes, so the check after it always has its 16. Then PyPI's JSON API must serve exactly the two verified files, by digest, neither yanked. That is the same script as TestPyPI's check, reading `INDEX`, `JSON_BASE`, `READS` and `PAUSE` from its step's `env`.

**Both uploads set `skip-existing: true`.** A re-run after a partial upload then uploads what is missing, instead of stopping on the file the index already holds. A skipped file could differ from the verified one, so the preflight refuses before any upload if the index already holds a file at any other digest, and an index never replaces a file. The JSON check after each upload then requires exactly the build job's two digests. A yanked file stays yanked, and an unpinned install skips it, so the preflight refuses a yanked file and the check after the upload does too, naming the remedy: un-yank it on the index, or release a new version (round 10).

**No PyPI token and no stored secret appear anywhere.** `id-token: write` is granted to `testpypi` and `pypi` only, and the workflow's default is `permissions: {}`. The one other credential is GitHub's own automatic `GITHUB_TOKEN`, passed to `gh` for reads of this repository's own API only. In the build job, it reads the compare with `main` and the environments (`contents: read`, `actions: read`). In each publish job, it reads that job's environment and this run's review history (`actions: read`). The openDox root's pin is read over git with no credential at all. Inputs reach every script through `env:`, never through an expression pasted into a script.

**Every action is pinned by full commit SHA**, each SHA resolved from its release tag through the git refs API:

| action | tag | commit |
|---|---|---|
| `actions/checkout` | v7.0.1 | `3d3c42e5aac5ba805825da76410c181273ba90b1` |
| `actions/setup-python` | v7.0.0 | `5fda3b95a4ea91299a34e894583c3862153e4b97` |
| `actions/upload-artifact` | v7.0.1 | `043fb46d1a93c77aae656e7c1c64a875d1fc6a0a` |
| `actions/download-artifact` | v8.0.1 | `3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c` |
| `pypa/gh-action-pypi-publish` | v1.14.2 | `dc37677b2e1c63e2034f94d8a5b11f265b73ba33` |

`validate.yml` pins by tag. This file departs from that on the brief's word: a tag can be moved after review, and a commit cannot. **One limit, accepted** (D8, F3): `pypa/gh-action-pypi-publish` is a container action, and called from any repository but its own it runs `ghcr.io/pypa/gh-action-pypi-publish:<sha>`, pulled by registry tag (its `create-docker-action.py`, read at `dc37677b`). So the step that holds the OIDC token runs an image that a tag names. The follow-on is a hash-locked `twine upload` with the documented manual OIDC token exchange. It edits no other workflow. openDox-code#75 (T095) edits `validate.yml` only, so the two do not overlap.

**`.github/release-tools-cpython312-linux.txt`.** It hash-locks `build==1.6.1`, `twine==7.0.0` and `setuptools==84.0.0`, with their dependencies (30 packages), compiled by `uv pip compile --generate-hashes` for cpython 3.12 on x86_64 linux. Its header gives the command.
- It is separate from `constraints-cpython312-linux.txt`. That file is this package's own resolved dependency tree, which `tests_runtime/test_deploy_shape.py` holds every install of the package to. These are the tools that build the package.
- `setuptools==84.0.0` is the version #69 pins there for the `test` extra.

**`tests/test_release_workflow.py`** was added at Copilot's request. It holds 129 hermetic cases that run the steps' own scripts, extracted from `release.yml`, as `tests/test_triple_pin.py` runs `validate.yml`'s pin:
- the shape: dispatch only, one input, no secret, `id-token` on the two publish jobs alone, each in its own environment, every action pinned by full SHA, and the job chain;
- the release-commit steps (10 cases, run as the build job runs them, the ref step then the pin step), with a stand-in `gh` first on `PATH` for the compare. Each case builds a stand-in openDox root and redirects the root's URL to it through git's `url.<base>.insteadOf`, set in the environment. It also sets `GIT_ALLOW_PROTOCOL=file`, so a redirect that failed would refuse rather than reach GitHub. A structural case holds the anonymous read: no `contents/` API call, the fetch line with `-c credential.helper=`, no `gh` call and no `env` in the pin step, and exactly one `gh` call in the ref step, which is the compare, against `refs/heads/main`. The fetch names `refs/heads/main` too;
- a tag named `main` in the root (6 cases): a stand-in root whose branch `main` pins the release commit and whose tag `main`, at a commit off the branch, pins another. In each of the three jobs, the branch's pin passes and the tag's pin refuses (round 11);
- the pin again before each upload: in each publish job, the step right before the upload is the build job's pin step. A moved pin refuses there, and an unchanged one passes, in each job (4 cases);
- the environment step (5 cases), with the stand-in `gh` answering each environment as the API does: both with a reviewer and a limit pass; no reviewer, an unreadable environment, no deployment limit, and protected branches in place of a limit each refuse;
- the version gate (10 cases);
- the artifact checks (17 cases), over a hand-built wheel and sdist in a git tree of their own. Their pyproject carries a platform marker on a base requirement, a parenthesized `or` marker on an extra's requirement, and a direct URL. Refused: a marker changed or dropped (an extra's, and a base requirement's), a direct URL in place of a version, a direct URL changed, and an extra under an `or`. Accepted: the extra's clause written first;
- the preflight, in both publish jobs: its place before the pin's re-check, the two jobs sharing one script, and 8 index cases each (no file yet, both files verified, one verified file from a partial upload, the sdist at another digest, a file nobody verified, a 503, the verified wheel yanked, and the release yanked);
- both publish jobs' digest check (6 cases);
- the build job's order (the artifact checks, the digests, the smoke test, the re-check, the upload) and the re-check of `dist/` after the smoke test (4 cases: unchanged passes; a changed byte, a third file and a missing file refuse);
- `skip-existing` on both uploads, the two index checks as one script, and PyPI's check as the step after its upload;
- the index check against a local JSON server, in both jobs: exact, lagging then exact, another sdist, a third file, a refused read, and the verified files with the wheel yanked (12 cases);
- each job's timeout and each step's, all read from the workflow: every step declares one, each job covers the sum of its steps', each upload is at most 10 minutes, and each retrying step covers its script's retries;
- the approval at use time, in both publish jobs: the step's place, `env` and the jobs' permissions, one script, and eight cases each (approved; the rule removed; the deployment limit removed; the environment unreadable; no approval; only the other environment approved; rejected; the history unreadable);
- the dry run's install: the line's parts (the falsifier's, plus `--report`), and that nothing from the venv runs but pip before the verdict. Nine verdict cases run over pip reports: the verified wheel passes; PyPI at, above and at the verified digest refuses; PyPI below is retried; TestPyPI older is retried; TestPyPI newer refuses; TestPyPI at another digest refuses; no `opendox` refuses.

**`validate.yml`'s floors are not moved.** They permit a rise, and `validate.yml` is T095's single-writer file (openDox-code#75), as every other phase-3 test addition has left it. The whole suite at this PR's head is in proof 9.

**`pyproject.toml`: `readme = "README.md"`** (the holder's decision). Without it, the metadata carries no long description and `twine check --strict` refuses both files. **README.md's four relative links are made absolute** (`SECURITY.md` and three `docs/` links), so they resolve from the PyPI page too.
- **pyproject.toml's single-writer order puts this edit after T072 (openDox-code#69) and T075 (openDox-code#73).** The line sits in `[project]`, away from both PRs' hunks.
- **`version` stays `0.0.0` here.** The bump to `0.1.0` (RULED `5963162921`) is openDox-code#79, the last phase-3 openDox-code landing before T087.

## Proofs

Every run below is local and never pushed. The harness parses `release.yml` and runs each job's `run` steps exactly as written, under `bash --noprofile --norc -eo pipefail`, with the `uses` steps emulated. **It refuses to emulate `pypa/gh-action-pypi-publish` and stops there**, so nothing is uploaded.

**1. The build job on this head's own tree.** The tree is `7cc65f93`, this branch merged with `main` `8e377823`, which carries #69 and #73. One local-only commit sets `version = "0.1.0"`. Round 6's verify step reads the same:

```
3. requirements: the local extra carries ['opendox[runtime]', 'pixeltable-pgserver<0.7,>=0.6.0']
5. web bundle: 42 of 42 tracked files are in the wheel
6. source tree: 118 of 118 tracked files under src/ are in the wheel; 0 other entries
7. migrations: 2 of 2 are in the wheel's data directory
opendox[local]: 34 distributions, every requirement installed and satisfied
initdb (PostgreSQL) 16.14
postgres (PostgreSQL) 16.14
opendox generate-and-open --help names --local
wheel-sha256=8e179862e1f6678eee9cd292f177d48718c40bc796eb1620a48ef7c0a3e4e00f
JOB build: every run step passed
```

The full run, before #69 and #73 landed: #73's head `0457b383` (which contains #69's head `28e195b9`), merged with this PR's fix-round commit `647d246e`, with the same local-only version commit. The harness runs the steps of this head's `release.yml` (`7cc65f93`) over that tree. The release-commit and environment steps run separately (4 and 5 below). Every other step passed:

```
version 0.1.0: pyproject.toml declares it
Checking dist/opendox-0.1.0-py3-none-any.whl: PASSED
Checking dist/opendox-0.1.0.tar.gz: PASSED
1. dist/: ['opendox-0.1.0-py3-none-any.whl', 'opendox-0.1.0.tar.gz']
2. metadata: Name opendox, Version 0.1.0
3. requirements: the local extra carries ['opendox[runtime]', 'pixeltable-pgserver<0.7,>=0.6.0']
4. console scripts: {'opendox': 'opendox.cli:main', 'opendox-runtime': 'opendox.runtime.cli:main'}
5. web bundle: 42 of 42 tracked files are in the wheel
6. source tree: 117 of 117 tracked files under src/ are in the wheel; 0 other entries
7. migrations: 2 of 2 are in the wheel's data directory
opendox 0.1.0 imports from .../runner-temp/fresh/lib/python3.12/site-packages/opendox
opendox[local]: 34 distributions, every requirement installed and satisfied
initdb (PostgreSQL) 16.14
postgres (PostgreSQL) 16.14
opendox generate-and-open --help names --local
wheel-sha256=3221e83d6b44457616b0f5db9689a7062475f5a77cd221b4eaf7dc92df1f34b7
sdist-sha256=3fdee8308c85abc85ad12ebeba0a1af5513eafa5fe3aa68d8e3c9d3b42ee9e01
JOB build: every run step passed
```

The wheel's digest is the same as the previous run's (`3221e83d…`), since `SOURCE_DATE_EPOCH` fixes its bytes. The sdist's contents are identical (`diff -r` of the two unpacked trees is empty), but its digest differs, because setuptools stamps the generated `PKG-INFO` and the gzip header with the build time. The workflow carries the digests of the one build it verified, so nothing depends on the sdist being reproducible.

The installed `opendox --help` still prints the carve-era `usage: ideation-dashboard` text. That is T084's to fix (openDox-code#77), not this PR's.

**2. The same job at this PR's head `2b0083b9`** (main `66ff7257` plus this branch, without #69 or #73), with the same local version commit. The verify step refuses, naming exactly what #69 and #73 add:

```
3. requirements: the local extra carries []
5. web bundle: 41 of 42 tracked files are in the wheel
6. source tree: 116 of 117 tracked files under src/ are in the wheel; 0 other entries
7. migrations: 0 of 2 are in the wheel's data directory
::error::the `local` extra carries [], not opendox[runtime] and the bundled server's package, pixeltable-pgserver
::error::the wheel lacks web files: ['opendox/web/vendor/.gitkeep']
::error::the wheel lacks tracked files: ['opendox/web/vendor/.gitkeep']
::error::the wheel lacks migrations: [...0001_identity_and_coordination.sql', ...0002_migration_state.sql']
JOB build: FAILED at step 9 (verify the artifacts)
```

**3. The version gate** refuses `0.2.0` against `0.1.0`, `v0.1.0`, `0.1.0+local`, `1!2.0`, `banana`, `0.1.0"; print(1) #`, the `0.0.0` placeholder, the pre-release `0.2.0rc1` and the development release `0.2.0.dev1`. It passes `0.1.0` against `0.1.0`, and the post-release `0.1.0.post1`. `tests/test_release_workflow.py` holds each case.

**4. The release-commit step against the live opensoft/openDox `main`**, which pins `047bb4fa394f3e1bf42466062a67ef18e99f8d6a` today. openDox-code `main` has since moved past it (now `1130e996`), which is the case T095 will make at the cut. The run below is fix round 3's single step. Round 5 splits it in two with the same scripts, and the pin step's live run follows the block.

The step's script was extracted from this head's `release.yml` and run with:
- `env -i`;
- an empty `HOME`;
- `GIT_CONFIG_GLOBAL=/dev/null` and `GIT_CONFIG_NOSYSTEM=1`;
- `GIT_TRACE_CURL` on.

The only token given was the `GH_TOKEN` the compare uses, and git never reads it:

```
== main, pinned
047bb4fa394f3e1bf42466062a67ef18e99f8d6a is the commit opensoft/openDox main pins for opensoft/openDox-code
047bb4fa394f3e1bf42466062a67ef18e99f8d6a is on main (ahead), dispatched on refs/heads/main
rc=0
== tag v0.1.0, pinned
... is on main (ahead), dispatched on refs/tags/v0.1.0
rc=0
== main, an unpinned commit
::error::this run is at 9a49040500000000000000000000000000000000, and the openDox root pins 047bb4fa394f3e1bf42466062a67ef18e99f8d6a; a release publishes only the pinned commit. ...
rc=1
== tag nightly
::error::a release dispatched on a tag names v0.1.0, and this run names refs/tags/nightly
rc=1
curl-trace (main): git requests=3 authorization headers=0
curl-trace (tag):  git requests=3 authorization headers=0
```

The three requests are `GET /opensoft/openDox.git/info/refs?service=git-upload-pack` and two `POST /opensoft/openDox.git/git-upload-pack`. None of them carries an `Authorization` header.

At this head, **the `pypi` job's pin step** (the same script as the build job's) was run the same way, with `env -i`, no token and no `python` but the system's `python3`:

```
== the pypi job's pin step, GITHUB_SHA=047bb4fa
047bb4fa394f3e1bf42466062a67ef18e99f8d6a is the commit opensoft/openDox main pins for opensoft/openDox-code
rc=0; git requests=3 authorization=0
== the pypi job's pin step, GITHUB_SHA=22222222
::error::this run is at 2222222222222222222222222222222222222222, and the openDox root pins 047bb4fa394f3e1bf42466062a67ef18e99f8d6a; a release publishes only the pinned ...
rc=1; git requests=3 authorization=0
```

A commit that is not on `main`, such as #73's head, reads `behind` on the compare call, and the step refuses it.

**5. The environment step today** refuses both environments with `gh: Not Found (HTTP 404)`, then `::error::the testpypi environment cannot be read (it does not exist yet, ...)`, and the same for `pypi`. Pointed at this repository's `copilot` environment, which has no protection rule, it prints `::error::the copilot environment names no required reviewer, ...`.

**6. The publish jobs' digest check** passes on the build job's artifact. It refuses a wheel with one changed byte and a third file in `dist/`, and the harness stops at each publish action.

**7. The index checks, live.** The one JSON script, extracted from the `pypi` job, was run against both indexes:

```
== TestPyPI sampleproject 1.2.0: sampleproject-1.2.0-py2.py3-none-any.whl sampleproject-1.2.0.tar.gz
TestPyPI serves exactly the verified files: {...}
rc=0
== TestPyPI sampleproject 1.2.0, the wheel's digest wrong
::error::after 2 reads, TestPyPI serves {...}
rc=1
== PyPI sampleproject 4.0.0: sampleproject-4.0.0-py3-none-any.whl sampleproject-4.0.0.tar.gz
PyPI serves exactly the verified files: {...}
rc=0
== PyPI sampleproject 4.0.0, the wheel's digest wrong
::error::after 2 reads, PyPI serves {...}
rc=1
```

**`testpypi-install`.**
- The install step's line, pointed at a local HTTP simple index that serves the built wheel, with `--extra-index-url https://pypi.org/simple/`, installs `opendox[local]` and runs `opendox --help`.
- Both run against TestPyPI itself only at the dry run.

**8. Linters.**
- `actionlint` 1.7.12: no findings, over both workflow files.
- `zizmor` 1.30.1, online audits, `--persona=pedantic`: `No findings to report.`

**9. The whole suite** at `d91fd5b9` (`main` `8e377823` merged in), in a venv with the `test` extra, as `validate.yml` installs it, the bundled PostgreSQL included:
- `tests/`: `3013 passed, 11 skipped`;
- `tests_runtime/`: `631 passed, 166 skipped`.

In the earlier suite venv, which lacked the `local` extra's `pixeltable-pgserver`, 5 tests failed and 2 errored at this head, each with `the local install's PostgreSQL server is not installed`. They fail the same way at `main` `8e377823` in that venv, so the failures are environmental.

At `b75205d4` (rounds 7 and 8 change `release.yml` and the test file only), `tests/` reads `3023 passed, 11 skipped`, and `tests/test_release_workflow.py` alone reads `93 passed`. `main` has since gained `90ac7033` (#72, T073), which touches `src/` and tests only: no `pyproject.toml`, no workflow and no lock. Mutants, each run against that file:

| mutant | result |
|---|---|
| the pre-release refusal removed | `2 failed` (round 3) |
| the URL redirect removed | `7 failed` (each case refuses instead of reaching GitHub; round 3) |
| the credential helper left in place | `1 failed` (round 3) |
| the token-backed `contents/` API read restored | `8 failed` (round 3) |
| no `skip-existing` on the TestPyPI upload | `1 failed, 56 passed` (round 4) |
| `testpypi-install` back at a 20-minute timeout | `1 failed, 56 passed` (round 4) |
| no per-try `timeout 300` on pip | `1 failed, 56 passed` (round 4) |
| the index check compares names, not digests | `2 failed, 55 passed` (round 4) |
| no PyPI check after the upload | `6 failed, 51 passed` (round 4) |
| no pin re-check in the `pypi` job | `3 failed, 59 passed` (round 5) |
| the `pypi` job's re-check ignores the commit | `2 failed, 60 passed` (round 5) |
| markers dropped from the requirement comparison | `4 failed, 78 passed` (round 6) |
| a direct URL ignored | `1 failed, 82 passed` (round 6; the changed-URL case was added for it) |
| a preflight that accepts any held file | `4 failed, 78 passed` (round 6) |
| no preflight in the `pypi` job | `7 failed, 75 passed` (round 6) |
| the PyPI upload unbounded; at 30 minutes; the `pypi` job at 30; PyPI's JSON check at 10 | each fails the timeout test (round 7) |
| the verdict ignores the host; ignores the digest; the venv's interpreter run before it; no `--report` | each fails (round 8) |
| any environment's approval counts; a rejected review counts; the rule not re-read; no approval check in `pypi` | each fails (round 9) |

**10. The approval check, live.** The step's script ran with `env -i` and a read token against a real run of OpsxFactory, whose `github-administration` environment is reviewer-protected:
- for `github-administration`, the environment that run deployed to, it passes: `a required reviewer is named, and this run's deployment was approved by brettheap`;
- for `endpoint-verification`, which that run did not deploy to, it refuses: `holds no approval of a deployment to endpoint-verification`;
- against this repository's `pypi`, which does not exist yet, it refuses: `the pypi environment cannot be read`.

**11. The verdict, on real pip reports.** In fresh pip 24.0 venvs, a two-index install of `sampleproject` resolved `4.0.0 from files.pythonhosted.org`: PyPI's file, which is the risk itself. A TestPyPI-only install resolved `1.2.0 from test-files.pythonhosted.org`. With each report's name rewritten to `opendox`, the step's verdict script refused the first (`... which is not the verified wheel`, rc 2) and passed the second (`opendox 1.2.0 is the verified wheel, from test-files.pythonhosted.org`, rc 0).

## Copilot's findings at `de3e3249`

- **The repository token and the public root** (r4171040914). The pin is now read anonymously over git, as above, so the read depends on no token.
  - You cite GitHub's documentation that the token is limited to the workflow's repository. The estate also has evidence that the token reads a public repository of the same org over git: openxFactory's `openreposhape-pin-gate.yml` run `37085426729` checks out public `opensoft/openRepoShape` with the default `github.token` (`Contents: read`, `Metadata: read`), and succeeds.
  - The REST call under that token was not measured, and the change makes the question moot.
- **Pre-releases and the unversioned install** (r4171040935). The version gate refuses them, through `Version.is_prerelease`. `VERSION_CASES` holds `0.2.0rc1` and `0.2.0.dev1` (both refused) and `0.1.0.post1` (accepted).

## Copilot's findings at `7cc65f93` and `74b87375`

- **A partial upload is not recoverable** ("previously missed" at `7cc65f93`): both uploads set `skip-existing`, and PyPI's served digests are now checked after its upload too (round 4, `74b87375`).
- **The retry budget exceeds the job timeout** ("previously missed" at `7cc65f93`): `testpypi-install` is at 75 minutes, its budget in full, with each pip try cut off after 300 s. A test recomputes the budget from the steps (round 4).
- **The pin can move before the PyPI upload** (r4171232529, at `74b87375`): both publish jobs run the build job's pin step again, right before their upload (round 5, `994a39d7`).

## Copilot's findings at `5cdb2ded`

- **Requirement markers were dropped before the comparison** (r4173500546): markers are now compared whole, removing only setuptools' extra clause, and direct URLs are compared too (round 6, `d7e9ff7b`, `d91fd5b9`).
- **`skip-existing` on PyPI could make a mixed release** (r4173500563): a preflight before each upload refuses a file the build job did not verify, and a verified name at another digest, before anything is uploaded (round 6).

## Copilot's findings at `d91fd5b9` and `960d549b`

- **The upload step had no bound of its own** (r4173861533), **and the test assumed one** (r4173861563). Every step after the build now declares its timeout, and the test reads each bound from the workflow (round 7, `960d549b`).
- **The dry run could install an `opendox` that PyPI serves** (r4173890172). The install line stays as written and adds `--report`. The report must show the verified TestPyPI wheel before anything from the install runs (round 8, `b75205d4`).

## Copilot's review at `b75205d4`

"Needs a closer look", with no findings and one item it had missed before: the dry run's comment said its job holds no token, but its checkout uses the job's `GITHUB_TOKEN`. The comment now says only the install step receives no token (`f59e7105`).

## The refresh of 2026-10-04: `main` `ca9e1bd5` merged, the head is `7ee2b425`

This PR stays DRAFT, and it lands last among phase 3's shipped-package changes. The refresh leaves only a final `main` merge for later.

- **The merges.** `main` `c4b55cc4` was merged at `d5dc825a`: #72 (T073), #77 (T084), #80 (T103), #81 (T102) and #85 (the T102 follow-on). `main` `ca9e1bd5` was merged at `7ee2b425`: #76 (T082). Both merges were clean. None of them touches `pyproject.toml`, the lock or `release.yml`. #76 re-pins `validate.yml`'s floors, and this PR's added cases only raise the counts above them; it adds no skip.
- **The whole suite at `7ee2b425`**, in a venv with the `test` extra, the bundled PostgreSQL included:
  - `tests/`: `3286 passed, 11 skipped`;
  - `tests_runtime/`: `632 passed, 166 skipped`.

  actionlint and zizmor (pedantic) report no findings.
- **The dry-run path, as far as it runs without publishing.** The tree is the release itself: this head merged with openDox-code#79 at its refreshed head `31361c91`, so `version = "0.1.0"` comes from #79 and not from a local edit. It is a local merge, never pushed.
  - **The `build` job** passes every run step: 42 of 42 web files, 119 of 119 source files, 2 of 2 migrations, the `local` extra's `opendox[runtime]` and `pixeltable-pgserver<0.7,>=0.6.0`, 34 distributions installed and satisfied, `initdb` and `postgres` 16.14, and `generate-and-open --help` naming `--local`. The installed `opendox --help` now prints `usage: opendox`, since T084 landed. The digests are `wheel-sha256=4f1a6969…` and `sdist-sha256=d0bf9fbd…`. The harness skips the release-commit and environment steps, which need the root pin and the environments, and it refuses to run the publish action.
  - **The publish jobs' read-only steps, live:**
    - the digest check passes on that artifact;
    - the preflight reads `TestPyPI holds no file of opendox 0.1.0 yet` and `PyPI holds no file of opendox 0.1.0 yet`, both reads of the public JSON APIs;
    - the pin step passes for `047bb4fa`, which the openDox root pins today, and refuses the trial's own commit with `a release publishes only the pinned commit`.

    The approval step was not run, because it needs the environments, which are Brett's setup.
- **Nothing was published or dispatched.**

## Copilot's finding at `f59e7105`

- **The reviewer check goes stale before the uploads** (r4174341950). Each publish job re-reads its environment's rule and this run's review history, right before its preflight, pin check and upload (round 9, `b3907858`).

## The final merge of 2026-10-05: `main` `651c35fe` merged at `e5f7f554`

- **The merge.** `main` `651c35fe` was merged at `e5f7f554`: #82 (T100, 16.3a), #84 (T104) and #86 (the T100 follow-on). It was clean, as a local rehearsal had shown before #86 landed. None of the three touches `pyproject.toml`, the lock, `README.md`, `release.yml` or the release tests. This PR touches neither `validate.yml` nor the web-boundary census, so the floors stay `main`'s.
- **What the three change in the release path: nothing that needs an edit here.**
  - #82's `doxbench_trust.py` and #84's `console_access.py` are new source files. The artifact checks count the tracked tree, so both are in the wheel with no edit (121 of 121).
  - The fresh-venv and TestPyPI install steps run only `--help` and never start the server. So #84's console copy under `OPENDOX_STATE_DIR` and #86's trust store are never written in CI.
- **The whole suite at `e5f7f554`**, in a venv editable from that head's own tree, with the `test` extra, the bundled PostgreSQL included:
  - `tests/`: `3984 passed, 11 skipped`;
  - `tests_runtime/`: `632 passed, 166 skipped`.

  The venv matters. The subprocess tests import whatever tree the venv's editable install names, so a venv editable from another checkout tests that checkout instead.

## Copilot's findings at `e5f7f554`, and round 10 (`7fc0c46e`)

Three findings, each ruled by the holder:

- **The digests were recorded after the smoke test** (r4182777667, fixed). The smoke test installs and runs third-party wheels, which the lock pins by version but not by hash. A dependency's code could rewrite `dist/` between the artifact checks and the recording, and the digests would then bless the new bytes. The digests are now recorded right after `verify the artifacts`, and a new step after the smoke test requires `dist/` to still hold exactly those bytes. The publish jobs' digest checks are unchanged.
- **The reviewer rule and the approval are not correlated** (r4182777733, declined, by design). The step refuses a deployment that waited on nobody. GitHub checks an approval against the rule in force when it is given. Only a repository admin can change the rule while the run waits, and that admin can edit this workflow too. The API offers no snapshot of the rule at the time of the approval.
- **A yanked file** (r4182777770, fixed). The preflight refuses a held file that is yanked, and the check after each upload refuses a yanked file at once. Both name the remedy: un-yank it on the index, or release a new version. Each script is still one script for both indexes. Both JSON APIs carry `yanked` on each file, as read from pypi.org and test.pypi.org.

Verification at `7fc0c46e`:

- `tests/test_release_workflow.py`: `119 passed`. actionlint and zizmor (pedantic) report no findings.
- **Mutants**, each run against the new tests:

  | mutant | result |
  |---|---|
  | the digest step moved back after the smoke test | `1 failed` |
  | no re-check after the smoke test | `5 failed` |
  | a re-check that compares names only | `1 failed, 3 passed` |
  | the preflight ignores `yanked` | `4 failed, 12 passed` |
  | the check after the upload ignores `yanked` | `2 failed, 10 passed` |

- **The whole suite at `7fc0c46e`**, in a venv editable from this head's own tree:
  - `tests/`: `3995 passed, 11 skipped`;
  - `tests_runtime/`: `632 passed, 166 skipped`.
- **The dry-run path, as far as it runs without publishing.** The tree is this head merged with openDox-code#79's head `9519cd8e` (`8bd254b2`, local, never pushed), so `version = "0.1.0"` comes from #79.
  - **The `build` job** passes every run step, the new re-check included (`dist/opendox-0.1.0-py3-none-any.whl: OK`). It reads 42 of 42 web files, 121 of 121 source files and 2 of 2 migrations. The `local` extra installs 34 distributions, all satisfied, and `initdb` and `postgres` 16.14 run. `opendox --help` prints `usage: opendox`. The digests are `wheel-sha256=70b98749…` and `sdist-sha256=830b0bb1…`. The harness now resolves `steps.<id>.outputs`, which the re-check's `env` reads.
  - **The publish jobs' read-only steps, live**, extracted from this head:
    - the digest check passes;
    - the preflight, with its new `yanked` check, reads `TestPyPI holds no file of opendox 0.1.0 yet` and `PyPI holds no file of opendox 0.1.0 yet`;
    - the pin step passes for `047bb4fa` and refuses `8bd254b2`.

    The approval step was not run, because it needs Brett's environments.
- **Copilot at `7fc0c46e`**: "Needs a closer look", with no findings. Its note is that the first publication depends on the environments and trusted publishers configured outside the repository. That is Brett's one-time setup, listed below.
- **Nothing was published or dispatched.**

## Lane 3's review D8 at `7fc0c46e`, and round 11 (`93d13217`)

Lane 3 reviewed this PR independently at `7fc0c46e` and reported four findings. The holder ruled them in openxFactory#656 comment `5992918154`.

- **F1, fixed: a tag named `main` in the openDox root could name the release commit.**
  - The cause: the three root fetches named a bare `main`, and git resolves that on the remote as `refs/tags/main` before `refs/heads/main`. A tag needs no review.
  - The fix: all three fetches now name `refs/heads/main`, and so does this repository's compare. The compare API accepts that form, as read live.
  - The test: a new case builds a stand-in root whose tag `main`, at a commit off the branch, pins another commit. In all three jobs, the branch's pin passes and the tag's pin refuses. Lane 3's own probe (`probe_tag_named_main.py`), run on this head, gives the same six verdicts.
- **F2, fixed: which ref may deploy was optional, and nothing checked it.**
  - The cause: a dispatch runs the dispatched ref's own `release.yml`, and PyPI's trusted publisher matches the repository, the workflow's file name and the environment, never the ref. Each environment's deployment limit is the one server-side control over the ref.
  - The environments, done: the holder set both on 2026-10-05. As read live, `testpypi` and `pypi` each name the required reviewer `brettheap`, with `deployment_branch_policy` `{"custom_branch_policies": true, "protected_branches": false}` and one policy, the tag pattern `v*`.
  - The workflow: the build job's environment step, and each publish job's use-time step, refuse when the environment has NO CUSTOM DEPLOYMENT LIMIT, that is, unless its `deployment_branch_policy` is set with `custom_branch_policies`.
  - **The tag-only limit is REQUIRED, and configured.** Its pattern, `v*`, is verified through the API (above), but the workflow does not re-read it (round 12, below). The release is dispatched on the tag `v0.1.0`, created at T087's pinned commit. A dispatch on `main` cannot deploy.
  - **The remaining limit, accepted:** while sessions act as brettheap, the approval click is the last line of defense. A ruleset over `v*` tags is recommended to Brett, not required.
- **F3, an accepted limit: the SHA-pinned publish action runs its image by registry tag.** The workflow's header now says so (see the actions table above). The follow-on is a hash-locked `twine upload` with the manual OIDC token exchange.
- **F4, an accepted limit, with a runbook line now: the sdist is not byte-reproducible.**
  - Proven again here: two builds of the same trial commit (`c86c31d0`), from two clones, give the same wheel (`ead0fae3…`) but two sdists (`876165d9…` and `cad6a569…`). Those two sdists unpack to identical trees, and differ in the gzip header's time and the members' times.
  - So after the TestPyPI upload, only "Re-run failed jobs" may be used, never "Re-run all jobs" and never a second dispatch: a rebuild would record new digests, and the preflight would then refuse 0.1.0 for good. The header and the cut steps below say so.
  - The follow-on is a reproducible sdist.

Verification at `93d13217`:

- `tests/test_release_workflow.py`: `129 passed`. actionlint (every workflow) and zizmor (pedantic) report no findings.
- **Mutants**, each run against the new tests:

  | mutant | result |
  |---|---|
  | the three root fetches name a bare `main` | `7 failed` |
  | the compare names a bare `main` | `1 failed, 10 passed` |
  | the build's environment step ignores the deployment limit | `2 failed, 3 passed` |
  | the build's environment step takes protected branches for a limit | `1 failed, 4 passed` |
  | each publish job's step ignores the deployment limit | `2 failed, 14 passed` |

- **The whole suite at `93d13217`**, in a venv editable from this head's own tree:
  - `tests/`: `4005 passed, 11 skipped`;
  - `tests_runtime/`: `632 passed, 166 skipped`.
- **The dry-run path, as far as it runs without publishing.** The tree is this head merged with openDox-code#79's head `9519cd8e` (`c86c31d0`, local, never pushed).
  - **The `build` job** passes every run step: 42 of 42 web files, 121 of 121 source files, 2 of 2 migrations, the `local` extra (34 distributions), `initdb` and `postgres` 16.14, `usage: opendox`, and the re-check of `dist/`. The digests are `wheel-sha256=ead0fae3…` and `sdist-sha256=876165d9…`.
  - **Read-only steps, run live:**
    - the digest check passes;
    - TestPyPI and PyPI hold no file of opendox 0.1.0;
    - the pin step, which now fetches `refs/heads/main`, passes for `047bb4fa` and refuses `c86c31d0`;
    - the ref step, as a dispatch on `refs/tags/v0.1.0` at `047bb4fa`, reads `is on main (ahead)` through the compare against `refs/heads/main`;
    - **the environment step**, now that the environments exist, reads `testpypi: 1 required reviewer(s), and only the refs its deployment limit names can deploy`, and the same for `pypi`. It ran with this session's own `gh` login, as a read only.

    The use-time approval step was not run, since it needs a run awaiting approval.
- **Nothing was published or dispatched.**

## Copilot's finding at `93d13217`, and round 12 (`07cc8f36`, text only)

- **The check proves a custom limit exists, not which pattern it names** (r4183379111). Copilot suggested reading each environment's `deployment-branch-policies` and requiring exactly the tag `v*`. The holder ruled it an accepted limit, with a text fix only (openxFactory#656 comment `5993462741`).
  - Why not read the patterns: the release would then depend on the job's `actions: read` token reading that endpoint, which is unverified. A refusal there would only show at the cut, after T087 has pinned the commit, and fixing it would need a new pinned commit.
  - The reasoning that declined r4182777733 applies: changing the pattern is an admin act, and an admin can edit this workflow too.
- **Round 12 changes comments only.** The header now says the build job refuses when either environment has no custom deployment limit, and each publish job checks that again. It says the pattern, the tag `v*` alone, is configured (by the lane, 2026-10-05) and verified through the API, but not re-read by the workflow. The build job's environment step says the same.
  - The parsed workflow is identical to `93d13217`'s.
  - `tests/test_release_workflow.py`: `129 passed`. With `tests/test_triple_pin.py` and `tests/test_web_boundary.py`: `148 passed`. actionlint and zizmor (pedantic) report no findings.
- r4183379111 is replied to and resolved.

## What Brett configures, once, before the first dispatch

1. **pypi.org.** Under Publishing, add a pending publisher (GitHub). PyPI project name: `opendox`. Owner: `opensoft`. Repository name: `openDox-code`. Workflow name: `release.yml`. Environment name: `pypi`.
2. **test.pypi.org** (its own account). The same pending publisher, with environment name `testpypi`.
3. **GitHub, opensoft/openDox-code, Settings, Environments: DONE** (by the holder, 2026-10-05, with `gh`). `pypi` and `testpypi` each name the required reviewer `brettheap`, with "Prevent self-review" off. **Each is limited to deployments from the tag pattern `v*` alone**, verified through the API. **A custom deployment limit is REQUIRED**: the build job and each publish job refuse an environment that has none. The workflow does not re-read the pattern itself, since changing it is an admin act (accepted, RULED `5993462741`). A dispatch on `main` or any branch cannot deploy.
4. **Recommended, not required:** a ruleset over `v*` tags. No ruleset covers tags today, so anyone who can push to this repository can create a `v*` tag. While sessions act as brettheap, the approval click is the last line of defense.

A pending publisher does not reserve the project name, and `opendox` is free on both indexes today, so the first publish should follow the configuration closely.

**At the cut**, after T087 has pinned the version bump, T089 has passed and AT-R1 has passed (T095, T096), on Brett's publish word:
1. Dispatch `release` with `version` = `0.1.0` **on the tag `v0.1.0`**, which the holder creates at the commit T087 pinned. A dispatch on `main` builds, but cannot deploy.
2. Approve `testpypi`.
3. When `testpypi-install` passes, approve `pypi`.
4. **After the TestPyPI upload, re-run only with "Re-run failed jobs", never "Re-run all jobs"**, and never dispatch the same version again. The sdist is not byte-reproducible, so a rebuild would record new digests, and the index keeps the first files for good (D8, F4).

A job that fails after an upload can be re-run from the same run. It reuses its artifacts, and `skip-existing` uploads only what is missing. "Re-run all jobs" or a second dispatch of the same version rebuilds. The wheel comes out byte-identical, but the sdist does not, and TestPyPI keeps the first files under the same names, so every later preflight refuses, correctly, and 0.1.0 could not complete without a version bump and a new T087 pin. After the publish verifies, T099's last step is a small openDox root PR that replaces the root README's "Where `opendox` comes from" paragraph with the PyPI install line (openxFactory#1220).

## Version

`pyproject.toml`'s `version` is `0.0.0` here. Release 1's version is **0.1.0**, RULED `5963162921`, *"0.1.0 (Recommended)"*: the first public release, with the API still pre-1.0. The bump is openDox-code#79.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@brettheap
brettheap marked this pull request as ready for review October 5, 2026 11:47
@brettheap

Copy link
Copy Markdown
Contributor Author

READY at 9519cd8 — Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

Holder: T101, T099's release step. version = "0.1.0" is its only change against main. It is an arc landing.

@brettheap
brettheap merged commit dede32b into main Oct 5, 2026
4 checks passed
@brettheap

Copy link
Copy Markdown
Contributor Author

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

LANDED — lane openxfactory-4, 2026-10-05T11:50:41Z, PR #79 → dede32b (opensoft/openDox-code main; plain gate)

Brett: land phase 1 / phase 2 PRs when green

brettheap added a commit that referenced this pull request Oct 5, 2026
Brings in #86 (T100 follow-on, the trust store's review findings), #78
(T099, the release workflow) and #79 (version 0.1.0). Main and this
branch touch no file in common, so the merge is clean. dede32b is the
commit that T087 pins in the openDox root (P).

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 5, 2026
…h no database service (plan 034) (#75)

Plan 034, task **T095**, slice `acceptance` (AT-R1, the HTTP half), phase 3. The plan is `specs/034-opendox-standalone-operation/tasks.md` at opensoft/openxFactory `main` `596a9a90`, § "Acceptance: AT-R1", as T007 batch N (`bdd0f586`) amends it. It realizes **FR-011's HTTP half**.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

- **Claim:** openxFactory#656 comment [`5961416730`](https://github.com/opensoft/openxFactory/issues/656#issuecomment-5961416730).
- **Rulings:**
  - [`5961364221`](https://github.com/opensoft/openxFactory/issues/656#issuecomment-5961364221) item 2: T095 is drafted now, and does not go READY before T063 lands.
  - [`5850003126`](https://github.com/opensoft/openxFactory/issues/656#issuecomment-5850003126): R1Q10 (a), R1Q12 (a), R1Q13 (a) with (c), R1Q15 (b), and R1Q16 (iii) and (iv).
  - [`5963851934`](https://github.com/opensoft/openxFactory/issues/656#issuecomment-5963851934): the console token travels in the opened URL, and the harness reads the private copy.
  - [`5973854291`](https://github.com/opensoft/openxFactory/issues/656#issuecomment-5973854291), F1 (i): the chat rail reads a thread only with a branch session (openDox-code#85).
- **State: DRAFT.** Its `After` line is T089, T076, T104 and T007 batch N. Batch N (openxFactory `bdd0f586`), T104 (openDox-code#84, `32943cbf`) and T076 (openDox#17, `504324de`) have landed. Its READY waits only on T089, lane openXfactory-3's phase-3 checkpoint. The holder posts READY and the landers merge; this writer does neither.
- **Release 1, for T096's evidence:**
  - **P** = `dede32b4b6f3d0f147d599776f83628c5af8ff3d`, the openDox-code commit T087 pins in the openDox root (`e1e3a3c3`).
  - **X** = this PR's landing commit, RELEASE1_TIP, which lane openXfactory-3's T096 evidence fills in.
  - **Build inputs: this PR changes none; it changes only CI and tests.** It touches `.github/workflows/validate.yml`, `acceptance/at_r1_http.py`, `tests/test_at_r1_http_harness.py` and `tests_runtime/test_deploy_shape.py`. It does not touch `pyproject.toml` or anything under `src/`. To check, both distributions were built locally the way `release.yml` builds them: the hash-locked release tools, `python -m build --no-isolation`, and `SOURCE_DATE_EPOCH` set to the commit's time. That was done at P and at `d29e68ca`. The two **wheels** hold the same 129 files, each with the same bytes. Built at P's `SOURCE_DATE_EPOCH`, `d29e68ca`'s wheel is byte-identical to P's (`sha256 8ecea00d…`). With each commit's own time they differ only in the archive's timestamps. The **sdist** at `d29e68ca` holds one file more, `tests/test_at_r1_http_harness.py`, because setuptools' default sdist takes `tests/test*.py`; its `SOURCES.txt` lists that file. So what installs at X is what installs at P, and the source archive differs by that one test file.
- **Convergence:** the lexer's and the resolver's residual misreadings are accepted limits of the harness, under the holder's ruling. They are listed, with the checks showing that no shipped file uses them, in "Accepted limits" below.

## On this PR, `acceptance` and `validate` are both green

The stack the harness drives has landed. `main` `dede32b4` (2026-10-05), which is P, carries:
- #60 (T071), #65 (T088), #67 (T070) and #69 (T072, the bundled server and the `local` extra);
- #71 (T085), #72 (T073), #73 (T075) and #74 (T081);
- #64, #77 (T084) and #80 (T103);
- #81 (T102), #85 (the T102 follow-on), #76 (T082) and #82 (T100);
- **#84 (T104, the console token via the opened URL)**, which landed on 2026-10-05;
- #86 (the T100 follow-on, the trust store's review findings), #78 (T099, the release workflow) and #79 (version 0.1.0).

The harness reads the console token the way T104 delivers it (step 6 below). Until T104 landed, this PR's `acceptance` job failed **by design**, at `[a.capabilities carries no console token]`. This branch now merges `main` `dede32b4` (P), so its own job runs the harness against the delivery it was written for, #86's trust store included. At this head `d29e68ca` it printed `AT-R1 HTTP half: PASS (314 assertions held)` (run 37316440405, job 111784259300). The `validate` job runs the whole suite and is green: `triple: selected=5379 passed=5368 skipped=11 failures=0 errors=0` (run 37316440405, job 111784258794).

**Making `acceptance` a required check is a ruleset change for the repository's owner** (`docs/branch-protection.md`). This PR does not touch the ruleset.

## What changes (four files, nothing else)

| file | change |
|---|---|
| `acceptance/at_r1_http.py` | NEW. The harness. Standard library only. Not a pytest module: it sits outside `tests/` and `tests_runtime/`, so `testpaths` never collects it and #1144's F9.1 is unchanged (FR-006's "no exclusion" holds). |
| `.github/workflows/validate.yml` | A new `acceptance` job, appended after the last line. It has **no services**, runs no pytest and installs nothing itself: one step, `python3 acceptance/at_r1_http.py`. The `validate` job and its triple are untouched. T082 (#76) re-pinned the triple's floors on `main` (3977 / 3966), and T100 (#82) changed only the `validate` job's comments; this PR merged both cleanly. |
| `tests_runtime/test_deploy_shape.py` | Required, because `test_the_workflow_declares_the_required_job_and_its_steps` pinned `sorted(jobs) == ["validate"]`. It now admits `["acceptance", "validate"]`. A new case holds `acceptance` to no service or container, no pytest run, no `pip install` step, and one harness step outside `testpaths`. And `test_every_install_of_this_package_reads_one_dependency_lock` now also requires that the harness passes the lock to pip. |
| `tests/test_at_r1_http_harness.py` | NEW. It tests the harness's DECISIONS in process, as `tests/test_smoke_signals.py` tests the browser half's oracle: it loads the harness from its file, against an in-process loopback server, so nothing is installed or started. **564 cases.** Every decision below has its cases, and 221 mutants of those decisions are each killed (below). |

The two test changes add cases to SELECTED and PASSED, and none skips; the floors permit a rise, and no floor is edited.

## The documented command, and how the harness runs it

T007 batch H's 10.3 addendum (`openspec/changes/add-neutral-product-standalone-operability/tasks.md`, 10.3, "AMENDED — T007 Batch H (`5850003126`; Ruled R1Q15 (b), R1Q16 (iii))"), verbatim:

> The command the root's `README.md` documents is the standalone install and its one start: `pip install "opendox[local]"`, then `opendox generate-and-open --local …`. The flag selects the local mode explicitly (13.4 as this batch amends it), and the `local` extra carries the bundled server that mode starts (13.1 as this batch amends it). After the install, that start is the single command requirement 10's second scenario has a user run. The entry point is still the code leg's console script, and no `Makefile` target is added, for the reason above. Carried out by T070 and T076.

The harness holds both lines as constants (`DOCUMENTED_INSTALL`, `DOCUMENTED_START`, the `…` being U+2026 after one space). Compared mechanically with T076's README (opensoft/openDox#17 at `7eef03b4`):

```
DOCUMENTED_INSTALL = 'pip install "opendox[local]"' (6f63616c5d22 tail bytes): README lines [32]
DOCUMENTED_START = 'opendox generate-and-open --local …' (616c20e280a6 tail bytes): README lines [33]
```

- **The start line.** The harness runs `opendox generate-and-open --local --repo-root <repo> --repository fixture --no-open --port <free port>`. That is quickstart.md § 3's filled form, with `--port` given a port found free at run time and checked free on 127.0.0.1 and ::1 just before the start. Before it starts anything, it asserts that its argv begins with exactly the tokens of `DOCUMENTED_START` before the `…` (`start.documented-command`). The `…` is the verb's own arguments, as the README says, and every flag above is one the README names. `opendox` is the fresh venv's console script, resolved on the child's PATH and asserted to lie inside that venv; argv[0] stays the literal `opendox`.
- **The install line.** No release of `opendox` is published (PyPI answers 404 for `opendox`; the release-channel question is with Brett). So the harness installs the same distribution with the same extra from the checkout: `pip install "<checkout>[local]"`, from a copy of this checkout's tracked files. That is exactly the substitution the README makes for its first line (`pip install "./code[local]"`, run from the root). It installs through `constraints-cpython312-linux.txt` where the lock's own interpreter runs it (CPython 3.12 on Linux, as in the job). The lock pins versions and adds no package, so the install is still `opendox[local]` and nothing else (AT-R1 step 2).

## What the harness asserts, in order (AT-R1 steps 1-4, and the route answers behind 5-8)

Each assertion has an id. The run stops at the first that fails and prints `AT-R1 HTTP half: FAIL [<id>]: <why>`, then exits 1. `--keep-going` reports every failure instead, and still exits 1. A harness that breaks before a verdict exits 2, never 0.

1. **Install.** A fresh venv, then `opendox[local]` (R1Q16 (iii)). Then `install declares the local extra`, read from the installed distribution's metadata.
2. **The clean machine, ASSERTED** (`clean.*`), in a fresh `OPENDOX_STATE_DIR` created empty for the run:
   - none of `openxdox`, `ideation_dashboard`, `doc_health` and `corpus_adapter_openxfactory` is importable in the venv (`python -I`, from an empty directory);
   - `omp` is not on the PATH, nor is the product's own `doxbench_bridge.HARNESS_COMMAND`;
   - no identity broker (`openprofiler-broker`) is on the PATH, and no issuer setting is set;
   - nothing listens on 127.0.0.1:5432 or [::1]:5432;
   - no PostgreSQL Unix socket answers in a shared directory. The harness reads every listening `.s.PGSQL.*` socket the kernel lists in `/proc/net/unix`, plus the distribution sockets as a fallback;
   - no DSN or `PG*` setting reaches a child;
   - neither repository holds a binding at the product's own `doxbench_binding.DEFAULT_BINDINGS_RELPATH`;
   - no bundled-server process exists before the start.

   Every child runs with `GIT_*`, `XF_*`, `OPENDOX_*`, `PG*`, `DATABASE_URL` and `PYTHONPATH` dropped, a fresh `HOME`, and `TMPDIR` inside the scratch directory. Before anything is installed, the harness also refuses a `TMPDIR` that another user could change (exit 2, by T104's own rules for the state tree; step 6), so its verdict on that tree judges what the product made of it.
3. **Two plain repositories**, each a fresh `git init` (spec.md AT-R1 step 3): (a) `tests/fixtures/plain-documents`, and (b) quickstart.md § 2's three notes, byte for byte, with no front matter. Each gets `git config user.name`/`user.email`, which the served actor is read from.
4. **The documented start**, once per repository (`start.*`): the port is free, the argv matches, the console script lies in the venv, the server is ready within 120 s, and the process that answered is still the one it launched.
5. **Fetches.**
   - `/` must be HTML by its type's essence (`text/html`, parameters aside), with an `<html>` element. It must also be UTF-8, as the harness reads it: a UTF-16 BOM, or a `charset` naming anything but a UTF-8 label, in its `Content-Type` or anywhere in the page, fails `[x.http / is UTF-8]`.
   - `/snapshot.json` must be a JSON object, non-empty, every document an object with a non-empty string `path`, and neutral per F5.3: none of #1144's fourteen declared words (5.5's falsifier list, verbatim) in any string value.
   - The grouping station, named by `/capabilities`' own display facet, must hold a tile with an id and a member document, so a grouping tile can open the chat pane (R1Q13 (a) with (c); this fails, and does not skip).
   - `/capabilities` must give `install.mode == "local"`. As quickstart § 3 asserts it (T007 batch N), its **raw** payload carries the console token **by name nowhere**: `console_token` is in neither the raw text nor any key of the parsed payload, however deep or however escaped (`[x.capabilities carries no console token]`). The by-VALUE half runs once step 6 has the token.
6. **The console token, as the user's browser is handed it** (T104). The start prints `console <file URL>`, the PATH of a private opener, and never the token. The harness reads that opener itself, with the standard library, and imports nothing from the product. The checks:
   - the start printed the opener, at `<OPENDOX_STATE_DIR>/console/<port>.html`, outside the served repository;
   - it is private: this user's regular file, mode exactly 0600, one link, in `console/` of mode exactly 0700 (as quickstart § 3 checks it). Its whole path is judged by T104's own rules for that tree: the state directory is this user's, and no one else can write it; every directory above it, as written and as resolved, is this user's or root's, and sticky where others can write it; every symbolic link on the way is this user's or root's;
   - it is opened with `O_NOFOLLOW | O_NONBLOCK`, read only if `fstat` calls it a regular file, and read WHOLE: a file past 64 KiB is refused by name, never judged by a truncated prefix;
   - it is a page the harness reads as the browser does: UTF-8 by the same rule as `/`, and holding no SVG, MathML, `frameset`, or refresh or script inside a `select`. Otherwise it forwards nothing, by name;
   - it has exactly one LIVE meta-refresh, as a browser that runs scripts parses the page. None inside a `<template>`, a `<noscript>`, a raw-text or escapable raw-text element, a `select`, or after a `frameset` counts. The first of a repeated attribute is read, `http-equiv` is `refresh` exactly, and `/>` closes only a void element. A tag that holds a named character reference with no `;` is not followed: `html.unescape` decodes it, and a browser's attribute rule does not;
   - that refresh is one a browser follows (the HTML standard's declarative refresh steps). It goes to this plane's console page (`/` or `/index.html`), on a loopback host the launched plane answers on, with no backslash, whitespace, control character or user information. `#console_token=<token>` is in the FRAGMENT, and never in the query or the path, raw or in any percent-decoding;
   - that token is one the page accepts: `[A-Za-z0-9_-]{16,512}`, read as T104's `takeDeliveredConsoleToken` reads it (`URLSearchParams` over the fragment);
   - its JSON record (`<script type="application/json" id="opendox-console">`, kind `opendox-console-access` v1, live, parsed as `JSON.parse` parses it) describes that same forward: its port, its token, its `opened_url` and its `page_url`;
   - the PAGE that forward opens is asked as the browser asks it: of the forward's own host (in `Host`, which the plane's loopback gate reads), with its path and query, and without its fragment. It must answer 200 as HTML (`[x.console page is HTML]`) and be UTF-8 (`[x.console page is UTF-8]`). Steps 7 and 8 read that page and its origin, never `/` in its place. T104's forward is `/index.html` on `127.0.0.1`;
   - and, BY VALUE (T007 batch N): the token is nowhere in `/capabilities`' raw payload, nor in any key or string value of the parsed one, under whatever name (`[x.capabilities carries the opener's token nowhere]`).
7. **The model catalog.** `/workbench/model-catalog` must refuse a caller with no token (4xx). With the opener's token in `X-XF-Console-Token`, it must answer 200 in the envelope the chat rail adopts (`views/doxbench-chat-model.js`, `adoptCatalog`):
   - `schema_version` is any JSON number equal to 1, never `true` and never `"1"`;
   - `kind` is `workbench-model-catalog`;
   - `models` is an array with no entry the rail would offer: none whose `available` is exactly `true`, as the rail reads it (16.4).

   The harness also asserts that the served bundle names both this route and this kind, so neither constant can drift from the bundle.
8. **Every route the panes can request**, asked at the console page's origin, answers, and none answers 5xx or drops the connection (derivation below). And the chat rail reads no thread: it reads `/workbench/thread` only through a branch-session column (`gate.workbench.session`, openDox-code#85), and the plane must contribute none (`[x.chat rail reads no thread (no branch session)]`).
9. **Stop and look.** SIGTERM to the entry point alone, as `kill` would, and a wait of up to 90 s. Then:
   - the entry point must exit 0;
   - no live process whose executable lies in the venv's `pixeltable_pgserver` package, or whose command line names the fresh state directory (R1Q16 (iv)), checked at the operating system;
   - the opener must be gone;
   - the token must appear nowhere in either output stream of the whole serve.

Cleanup runs however the run ends, but only AFTER the verdict is taken, so a leftover still fails the run.

**No line the harness prints quotes a console token.** Every token the run has seen is blanked out of every id, reason, note and the report (`Verdict.redact`), as it is and inside any run of `%XX` escapes that decodes to it, partly encoded or encoded twice: the tokens the opener carries, delivered or refused, and its record's; and one `/capabilities` publishes, under its name at any depth. `serve_one` reads the opener's tokens as soon as the start answers, before step 5 quotes anything (T104 writes the opener before it serves). No reason quotes the server's own bytes either: not the entry point's output, a `/capabilities` payload, a catalog body or its envelope's values or an available entry's id, the snapshot's documents or its grouping field, the opener record's values, a `<base href>`, a peer's error text, a printed opener path other than the expected one, or a `Content-Type` that is not a plain MIME type. Every printed line escapes what is not printable (a control, a newline, a bidirectional override, a lone surrogate), so no text a server sends can forge a line of the log or break one.

**Every parser reads as the browser reads, or refuses by name what it does not model:**
- JSON is parsed as `response.json()` parses it: a BOM dropped, an invalid byte read as U+FFFD, and `NaN` and `Infinity` refused. A body nested past the parser's depth is a named failure, never exit 2.
- JavaScript is lexed by its own whitespace (U+FEFF and every space separator) and line terminators, a hashbang line is a comment, and its strings are decoded with its escapes. An escape a module refuses is a named failure.
- HTML is read with the first of a repeated attribute, and live elements only. A script is what its `type` and `language` make it. SVG, MathML, a `frameset`, a script, link or base in a `select`, and one whose attributes hold a named reference with no `;` (`&copy=2`, which `html.unescape` decodes and a browser does not) are refused by name. A page is read as UTF-8, and refused by name where a browser might read it otherwise.
- URLs are resolved against the running server's origin. A reference a browser reads differently is refused by name: a backslash, a tab or newline, whitespace or a control at an end, user information, or a percent-encoded dot segment. So are a `<base>` other than `/` and an import map.
- A string from JavaScript or JSON is read in Unicode scalar values, as a browser reads a URL from it: a surrogate pair joined, and a lone surrogate read as U+FFFD.
- Request targets are percent-encoded as a browser encodes them. A `Content-Type` is parsed as the MIME Sniffing standard parses it, and one with more than one value is refused by name. A redirect is refused by name, because a browser follows it.

## How the route list is derived (from the served JS, not by hand)

`derive_bundle` reads the bundle **from the running server**, resolving against its origin (`http://127.0.0.1:<port>`):

1. It fetches `/`, then follows the page's LIVE module scripts (`<script type="module" src>`, `type` and `language` read as the HTML standard reads them) and `rel~=stylesheet` links. Both resolve against the page's base URL: its `<base href>`, which must be `/`, or the page's own. A view module `/capabilities` declares is walked too, but it is no entry:
   - a page with no live module script is `[bundle.entry]`;
   - a classic script, by `src` or inline, is `[bundle.classic-script <src>]`, unless it is marked `nomodule`;
   - an inline module script is `[bundle.inline-module]`;
   - markup the harness does not model is `[bundle.unmodeled]`.
2. In every module, a small JS lexer strips comments and regular-expression literals, decodes JavaScript's string escapes, and yields each string literal, in Unicode scalar values, with the code before it. Every run of JavaScript whitespace, and every comment, reads as one space there, so no run is too long. A template's `${…}` is read as code, starting an expression, so an import or a route inside one is found. A `/` divides after an operand (a name, a literal, a call, a group, a postfix `++` or `--`), and opens a regular expression after a prefix operator or the `)` of an `if`, `while`, `for` or `with` condition. After a `}` it does either, by whether the `}` ends a block or an expression, which a lexer cannot tell: a module with a `/` right after a `}` is `[bundle.ambiguous-slash <path>]`. A keyword is read as one only where it is no member's name (`obj.return`, `obj?.of`) and no part of a longer or a private name (`ñreturn`, `this.#if`); after a spread's `...` or a decimal point (`1. in`) it still is. `of` is a keyword only as a `for` head's separator, after its binding or target (`for await (` opens a head too), and a name elsewhere, a classic loop's initializer, condition and update included; a `/` after a spread's `...`, a division's own `/`, `export default`, `extends`, or `break`, `continue` or `debugger` and a line break opens a regular expression. So does one after a line break that ends a statement by automatic semicolon insertion: before a `++` or `--` (then a prefix), and after a labelled `break` or `continue`, a declaration's lone binding, or a static import's specifier. That code tells an `import … from "x"`, an `export … from "x"` or an `import "x"` from an ordinary string, and from a method named `import` (`loader.import("x")`). A module holding an escape a module refuses (a legacy octal escape, `\8`, `\9`, or a malformed `\x` or `\u`, outside a tagged template) is `[bundle.syntax <path>]`.
3. It follows every static specifier, every literal dynamic `import("x")`, and every view-binding module `/capabilities` declares, transitively. Each module is fetched once, and judged once per way it is imported:
   - A static import must answer 200, even of a path another module imports dynamically. A failed one is a module-load `pageerror`, which AT-R1 step 8 forbids.
   - A dynamic import may be refused, but not with a 5xx. Standalone, `/views/intent-feed.js` answers 404 (10.2a, not owed), and its importer degrades.
   - A served module must be JavaScript by its type's essence, and a linked stylesheet `text/css`. A redirect is refused by name, never judged by its status.
   - A BARE specifier is `[bundle.bare <specifier>]`. A module or stylesheet from another origin (another host, port or scheme, `data:`) is `[bundle.external <url>]`, and is never fetched from loopback by its path. A same-origin absolute URL (`http://127.0.0.1:<port>/x.js`) is a path of this plane.
4. In every module of that graph, every string literal that is a same-origin path (`/` or `./` followed by a letter, not a `.js`/`.css` path) is a route the bundle can request. At the integration below, that is **38 modules and 18 routes**.

A static read cannot tell a load-time request from an on-click one, so the harness requests ALL of them, a superset of the wheel's, the lens's and the chat rail's load-time reads. T096's browser run observes the load-time set itself. The nine `/actions/…` routes get a GET too; a GET there finds no handler (404), so it never executes an action.

A prefix ending in `/` (`/source/`) is completed with each document the snapshot lists, plain and keyed by the workbench's key (`/source/fixture%40main/<doc>`), as the panes complete it.

The thread read (`/workbench/thread`) is asked as its bare literal only, as every literal is. Since openDox-code#85, a standalone plane's rail sends no thread read at all, and the harness no longer completes it with the query it once claimed the rail sends (the holder's T096 dry run, F4).

Every request carries the console token the opener delivered, as the doxBench transports do, so a guarded read answers from its handler and not from the console check.

## Accepted limits (the holder's convergence ruling)

The harness reads the bundle with a lexer, not a parser. The holder ruled, under the rule of openxFactory#656 comment [`5988818366`](https://github.com/opensoft/openxFactory/issues/656#issuecomment-5988818366) (written for #86 and applied to this PR), that misreadings only a parser could fix are recorded as accepted limits of the acceptance harness, not chased round after round. A form is fixed now only if a shipped openDox web file contains it, or if the fix is one line with a test. The limits are recorded in `JsStrings`'s docstring and in `_resolve`'s.

**The lexer** (`JsStrings`):
- A `/` right after a `}` divides after an expression's `}` and opens a regular expression after a block's. Such a module is refused by name (`[bundle.ambiguous-slash <path>]`).
- In one form, the `/` is read wrongly. After a declaration list's last binding with no initializer, then a line break (`let x, y` [line break] `/re/`), automatic semicolon insertion ends the declaration, so the `/` opens a regular expression. The lexer reads a division, and an import after it on that line can be missed. Telling that comma from an expression's comma takes knowing that it lies in a declaration.

**URL resolution** (`_resolve`). `urllib.parse` reads a few references differently from the URL standard, and these are not refused:
- `///a.js` and `http:///127.0.0.1:<port>/a.js`;
- an empty path segment (`.//a.js`);
- a dot segment or an empty path in an absolute same-origin URL;
- an unencoded `^`;
- a loopback host that the URL standard rewrites (`127.1`, `0x7f.0.0.1`, `2130706433`, `127.0.0.1.`), which is judged an external host.

**No shipped file uses any of them.** These checks ran locally at `d29e68ca` over the 42 files the build packages (`web/**`, `web/**/.*`): 39 modules, the page, its sheet and `vendor/.gitkeep`.
- The lexer against acorn 8.18's exact token stream (a full module parse): they agree on all 205 `/` decisions, 132 regular expressions and 73 divisions. They agree on all 82 static imports and export-froms. No module is ambiguous or malformed. No regular expression follows a name, a literal, a `)`, a `]` or a `}` (3 follow `return`), so the residual form, which needs a binding name and then a line break before the `/`, occurs nowhere.
- The resolution against node's WHATWG `URL`: all 84 of the shipped modules' import specifiers, static and dynamic, resolve alike.

**How the limits were found.** A differential check of the lexer against node's own module parser, the static module requests of a `vm.SourceTextModule`. It covers 196 contexts, each a regular expression or a division before a static import, with five kinds of separator: 1960 snippets, 978 of them valid modules. In those, the lexer hid the import in 195 at `ff04e015`, in 85 at `09d9d632`, and now in the 10 of the residual form. It has no false failure at any of these heads, and refuses the same 10 by name at each. A second check, of 81 contrived references against node's WHATWG `URL`, finds 13 differences: the 12 that the URL list above records, and `//a.js`, which is an external URL either way. The overview at `e4f81d48` named identifier-parsing errors; that class was a one-line fix with a test, so it is fixed in `d698c6ea`.

## Falsifier: the harness itself

### At this branch, `main` `32943cbf` plus the harness (what this PR's own `acceptance` job runs): PASS

The job at `d29e68ca` (run 37316440405, job 111784259300):

```
AT-R1 HTTP half: PASS (314 assertions held)
```

Locally, the same tree, run as that job runs it (`python3 acceptance/at_r1_http.py` from the checkout's root, with no database service; `r83`, at `d29e68ca`, which merges `main` `dede32b4`):

```
AT-R1 HTTP half: PASS (314 assertions held)
```

Its log is line for line the same as `r82`'s (`d698c6ea`, before `dede32b4`) and `r76`'s (`ff04e015`, the first head with `main` `32943cbf`), apart from ports, process ids and scratch paths. That includes `[a.capabilities carries no console token]` and every console-opener check.

The `validate` job's suite, run locally at `d29e68ca` with the job's install line and a PostgreSQL 16 standing in for its service, printed `triple: selected=5379 passed=5368 skipped=11 failures=0 errors=0`. So no change since then moved a module, a route or an assertion. Neither log holds a token-shaped run.

The first green `acceptance` job ran at `d50e8cef`, once #84 had landed. Its merge ref with `main` `32943cbf` (`ff68275a`) gave `PASS (314 assertions held)`, and so did `r75`, locally, on a merge of the same two commits whose tree is identical to CI's.

### Before T104 landed: FAIL, named

At `main` `38d3350e` plus the harness (`d50e8cef`, `r74`, `--keep-going`), the harness failed only where T104 was missing, in both repositories. In CI it stopped at the first of these: `1 failed, 34 held`.

```
== r74-r27-pr-tree-keepgoing rc=1 held=282
AT-R1 HTTP half: FAIL [a.capabilities carries no console token]: …
      also FAIL [a.console opener printed]: the start printed no `console <file URL>` line naming the opener, …
      also FAIL [a.catalog answers]: /workbench/model-catalog with no console token to present (step 6 found none) answers HTTP 403 (its body is not quoted)
      also FAIL [b.capabilities carries no console token]: …
      also FAIL [b.console opener printed]: …
      also FAIL [b.catalog answers]: …
      6 failed, 282 held
```

No token-shaped string appears anywhere in that log, though that tree's `/capabilities` published the token.

The expected red moved as the stack landed:
- at `main` `047bb4fa` and `66ff7257`, it was `[install declares the local extra]`;
- once #69 landed, it was `[a.capabilities install.mode == local]`;
- from #72, it was the token delivery above, which only T104 supplies;
- since #84 landed (`32943cbf`), it is green.

### Against T104 before it landed (local integrations, never pushed): PASS

**#84 `fb8a1cc4` with `main` `38d3350e` merged in** (`fc9b39b5`, local only). #84 already merges `main` `ca9e1bd5`. The merge's one conflict was the web census's class-A total, resolved as base plus both deltas (18486 + 40 + 95 = 18621). With `b7b9b843`'s harness, `--keep-going` (`r71`):

```
== r71-r26-on-live15-t104-keepgoing rc=0 held=314
AT-R1 HTTP half: PASS (314 assertions held)
```

Each earlier head of this round passed at the integration of its day:
- `r73` (`d50e8cef`'s harness) and `r69` (`0717f72f`'s): the same integration, 314 held;
- `r67` (`3392f934`'s), `r65` (`9229c659`'s) and `r63` (`ba216f84`'s): the same integration, 310 held, before the console page checks;
- `r61` (`4bdb41fb`'s) and `r59`: #84 `fb8a1cc4`, 306 held, before the UTF-8 and entry checks;
- `r57` and `r58`: #84 `182cac76` plus `main` `ca9e1bd5`;
- `r53`: #84 `d4b99436` plus `main` `c4b55cc4`, after #85;
- `r49`: #84 `d4b99436`.

**Verdict at `main` `32943cbf` with this branch: AT-R1's HTTP half passes, with 314 assertions held.** That covers:
- the install, the clean machine and both repositories;
- the documented start: ready, with the process the harness launched;
- `/` as HTML, and UTF-8;
- the neutral, non-empty snapshot with a grouping tile: repository (a) has 8 documents, and repository (b), with no front matter, has 3;
- `install.mode == local`, and the token on `/capabilities` neither by name nor by value;
- the opener: printed, at `<OPENDOX_STATE_DIR>/console/<port>.html`, private down its whole path, a page the harness reads as the browser does, forwarding with the token in its fragment, and with a record that agrees;
- the page it opens, `/index.html` on `127.0.0.1`: 200, HTML and UTF-8, and the page whose bundle, routes and catalog the rest of the run reads;
- the catalog: it refuses a caller with no token, and with the token answers in the envelope the chat rail adopts, with no available entry;
- one live module script as the entry, and 38 modules, none bare, divergent, malformed or from outside the plane, every one served as JavaScript and every static import 200; `/views/intent-feed.js` refused as 10.2a's not-owed dynamic import; `/styles.css` served as `text/css`; no `<base>`, no import map, no classic or inline script, and nothing unmodeled;
- all 18 derived routes and every document's `/source/` form below 500, none a redirect, and no branch-session column, so no thread read;
- the stop: exit status 0 within the bound, no bundled PostgreSQL process left, the opener removed, and the token in neither output stream.

Pass (a)'s route answers (`r76`):

```
/actions/{dtn-seed,edit,notebook,refresh,staging-seed}                        HTTP 404
/actions/workbench/{chat-turn,document-abstract,model-approval,model-intake}  HTTP 404
/capabilities HTTP 200       /logout HTTP 404            /snapshot-index.json HTTP 404
/snapshot.json HTTP 200      /source/ HTTP 404           /source/<each of 8 documents> HTTP 200
/source/fixture%40main/<each of 8 documents> HTTP 200    /project-register.json HTTP 404
/workbench/model-catalog HTTP 200    /workbench/model-intake HTTP 200    /workbench/thread HTTP 400
```

### Before T084: two product gaps, both T084's, both removed by #77

The same harness against the integration without #77 (`e7187f06`: main + #69 `f66e5f82`, #72 `1b0c3a63`, #73 `71d24af6`, #71 `83213eb2`, #74 `9061b22a`, #65 `c0a97648`) held 198 and failed 4. The 4 were these two gaps, each hit in both repositories:

```
== r6-integ-keepgoing rc=1 held=198
AT-R1 HTTP half: FAIL [a.route /project-register.json]: GET /project-register.json answers no answer (RemoteDisconnected: ...)
      also FAIL [a.route /workbench/thread?repository=fixture&ref=main&tile_kind=cluster&tile_id=grouping-compost-corner&document=grouping-compost-corner.md]: ... no answer (RemoteDisconnected: ...)
      also FAIL [b.route /project-register.json]: ... no answer (RemoteDisconnected: ...)
      also FAIL [b.route /workbench/thread?repository=fixture&ref=main&tile_kind=cluster&tile_id=budget-roadmap&document=budget.md]: ... no answer (RemoteDisconnected: ...)
      4 failed, 198 held
```

1. **`GET /project-register.json` dropped the connection.** `src/opendox/serve_project.py:271` (at `main` `047bb4fa`) did `from openxdox.gate_console import DEFAULT_RECORDS_DIR` (and `:272`, `from openxdox.kickoff import …`), so it raised `ModuleNotFoundError: No module named 'openxdox'`. The page requests this route on every load (`app.js:1004` → `views/repo-selector.js:138-139`). It is batch L's named crash site (`5920216845`).
   **Removed by #77.** The handler now reads `column_seams.gate` and `column_seams.kickoff`. openDox's default discovers no register, so the route answers the existing structured `404 "no project register"`, and the picker hides.
2. **The chat rail's thread read dropped the connection.** `src/opendox/serve_workbench.py:560` at the pre-T084 integration (`:548` at `main`) did `from openxdox import doxbench_scope`, which is reached once the five query fields are present. The rail requests it when it opens on a document (`views/staging-workbench.js:2925`). It was **not** among batch L's three named crash sites; the holder relayed it to T084's writer.
   **Removed by #77.** The handler now uses openDox's own scope type. With no live session on that scope, it answers `403` with the no-live-session absence body.

No `from openxdox` import is left in either file at `d05c266e`, now that #77 has landed on `main`, and the runs with #77 found **no new gap**.

### The assertions bite: twelve product mutants, four workflow mutants and 221 decision mutants, all killed

Product mutants, each run through the full harness: m1, m2, m4 and m5 at `b4fc637f`, and m3 again at `b440d12b`. The only harness change since `b4fc637f` is `main()`'s handling of an error while preparing. m1 and m2 were merged from `main` when #69 was at `fedfa75d` and #72 at `20032d02`. m3 to m5 are working-tree edits of the integration as it was then, `8e4203d8` (before T084):

| mutant | tree | killed at |
|---|---|---|
| m1, no T081 | integration without #74 (`25f1f6af`) | `[a.catalog offers no available entry]`: "no model is configured, yet the catalog offers ['omp-local']" |
| m2, no T073 | integration without #72 (`f3957515`) | `[a.capabilities install.mode == local]`: "the served install block is None" |
| m3, the server is never stopped | `bundle.BundledServer.stop()` returns at once, and no parent-death signal | `[a.stop leaves no bundled PostgreSQL process]`: "still running after the entry point stopped: pid 599656: …/pixeltable_pgserver/pginstall/bin/postgres -D …" (and the harness's cleanup then removed it) |
| m4, a governance word in the fixture | `title: A slow leak at the rain barrel, ratified` | `[a.snapshot neutral (F5.3)]`: "openxFactory's vocabulary leaked into the neutral snapshot: ['ratified']" |
| m5, a missing module | `src/opendox/web/views/lens-model.js` deleted | `[a.bundle.module /views/lens-model.js]`: "imported statically by /app.js, answers HTTP 404" |

T104's delivery, each a working-tree edit of the integration WITH #84, `a945ef01`, run through the full harness at `d53a7378` (`r33`):

| mutant | edit | killed at |
|---|---|---|
| m6, a readable opener | `console_access.PRIVATE_MODE = 0o644` | `[a.console opener is private]`: "… has mode 644, not 600" |
| m7, the token in the query | `opened_url` joins with `?` instead of `#` | `[a.console opener forwards with the token in its fragment]`: "the opener's forward carries the token in its QUERY …" |
| m8, the opener outlives the server | `remove_private_copy` returns at once | `[a.stop removes the console opener]`: "… is still there after the server stopped …" |
| m9, the token back on `/capabilities` | `build_server` publishes it on every plane | `[a.capabilities carries no console token]` |
| m10, the token printed | `generate-and-open` prints `token <token>` | `[a.console token never printed]` |
| m11, an unguarded catalog | the catalog's console check removed | `[a.catalog refuses a caller without the console token]`: "… answers HTTP 200 …" |
| m12, the token on `/capabilities` under another name | `build_server` adds `capabilities["session_hint"] = console_token`, at #84 `d4b99436` (`r51`) | `[a.capabilities carries the opener's token nowhere]`, and no token in the log |

The harness's own decisions: **221 mutants**, each a one-line edit of `acceptance/at_r1_http.py` at this head, run against `tests/test_at_r1_http_harness.py`. **All 221 are killed**, each at a named case. By family:
- the inert content of a page (`lv1`-`lv9`);
- the state tree's rules and the `TMPDIR` refusal (`tr1`-`tr8`), and `console/` at 0700 exactly (`dm1`);
- the redaction (`rd1`-`rd9`), the published token at any depth (`nv1`), the payloads never quoted (`fo1`, `im1`), and the tokens learned before step 5 (`pk1`);
- the raw payload by name and by value (`rp1`-`rp6`), and the by-value check run (`rw1`);
- the chat rail's thread read (`th1`-`th3`);
- the opener read whole (`ol1`, `ol2`), the bodies and peer text never quoted (`cb1`, `cb2`, `en1`), and the `Content-Type` shown only as a plain type (`ct1`);
- the plane's own origin (`og1`-`og5`);
- JSON as the browser parses it (`js1`, `jc1`, `bj1`, `bj2`);
- divergent references (`dv1`-`dv4`, `ui1`), and encoded dot segments only (`ed1`-`ed5`); JavaScript escapes (`le1`-`le4`), line comments and a hashbang (`le5`, `le6`), and escapes a module refuses (`ms1`-`ms8`); the page's links (`ht1`, `il1`, `il2`, `bs1`, `im2`); encoded targets (`bt1`); unterminated references (`ur1`); and availability as the rail reads it (`av1`);
- only a live module script is an entry (`sr1`-`sr10`), and markup the harness does not model refused (`um1`-`um6`, `um6` an attribute's unterminated reference);
- the page walked is the one the opener opens, asked of its own host with its query, as HTML and UTF-8, with its modules, sheets, routes and catalog at its origin (`cp1`-`cp12`), an IPv6 host bracketed (`og6`), and the page's links resolved against its base URL (`bd1`, `bd2`);
- Unicode scalar values (`su1`-`su4`), and every printed line escaped (`pr1`-`pr5`);
- JavaScript's whitespace, collapsed, and a method named `import` (`ws1`-`ws4`, `ff1`); a template's `${…}` read as code, starting an expression (`tp1`-`tp3`); a `/` read as division or a regular expression by what ends before it (`rg1`-`rg6`), and one right after a `}` refused (`am1`, `am2`); a member's name never a keyword (`km1`-`km4`), a spread's and a decimal literal's `.` no member access, and a name read whole, a private one with its `#` (`km5`-`km16`); every keyword read by where it stands, `of` only in a `for` head, `for await (` a condition, a spread before an expression (`kw1`-`kw15`), and `of` the separator only after a `for` head's binding or target (`os1`-`os10`); a regular expression after a division's `/` (`pu1`) and after a line break that ends a statement (`lb1`-`lb12`); and every character of a name read as the name's (`id1`);
- a redirect refused (`rx1`, `rx2`), a repeated header joined (`hj1`), and `Content-Type` parsed as a browser parses it (`mt1`-`mt3`);
- UTF-8 pages (`cs1`-`cs7`);
- redaction through percent-encoding (`re1`, `re2`), and no reason quoting a value the catalog, the snapshot, the record or `/` sent (`cq1`, `cq2`, `sq1`, `gq1`, `rq1`, `rq2`, `bq1`).

Earlier heads' decision mutants (64, from `d53a7378` to `64dc06f5`) were each killed at their own head; their decisions stand, and their cases still pass.

Workflow mutants, against the three affected cases of `tests_runtime/test_deploy_shape.py`:

| mutant | killed by |
|---|---|
| `acceptance` gains a `postgres` service | `test_the_acceptance_job_runs_the_harness_on_a_clean_machine` |
| the harness drops `-c <lock>` | `test_every_install_of_this_package_reads_one_dependency_lock` |
| `acceptance` chains `&& python -m pytest -q` | `test_the_acceptance_job_runs_the_harness_on_a_clean_machine` |
| `acceptance` gains its own `pip install .` step | both cases |

Restored: `3 passed`. Locally, `tests_runtime/test_deploy_shape.py` and `tests/test_triple_pin.py` give `115 passed`.

### How the harness is run, and where the bundled server's state lives

The harness takes **no path and no port** (only `--keep` and `--keep-going`). It installs the checkout its own file is in, and its scratch and `OPENDOX_STATE_DIR` are fresh `tempfile` directories under `TMPDIR`. To measure an integration or a product mutant, this writer copied the harness, untracked, into that tree's `acceptance/` and ran it there.
- **In CI**, they sit in the runner's temporary directory, `/tmp`, which is sticky and root's.
- **The socket path is capped.** The bundled server's socket is `<OPENDOX_STATE_DIR>/postgres/run/.s.PGSQL.5432`, and Linux takes at most 107 bytes. A run whose socket could not fit is refused with exit 2, before anything is installed.
- **The local runs above** used `TMPDIR=~/.local/state/t095-tmp`, a 0700 directory whose every ancestor is this user's or root's. It was empty after every run, and no bundled PostgreSQL started by these runs was left running.

## Review rounds

- **SonarCloud** (advisory; the gate failed at `1c064bb3` on Security Rating D, and at `0717f72f` on Security Rating B, and passes at every other head since `bbdeb9ec`). At `0717f72f`, four `python:S5332` at `plane_origin`, which built an `http://` literal around the forward's host: fixed in `b7b9b843`, which builds the origin with `urlunsplit`. Fixed in `bbdeb9ec`:
  - `pythonsecurity:S8703`, `S8705`, `S8707`: the `--port`, `--scratch`, `--state-base` and `--checkout` options flowed into a socket, the server's argv and the filesystem. They are gone.
  - `python:S5443`: the `/tmp/.s.PGSQL.5432` probe is replaced by the distribution sockets.
  - `python:S3776`: the lexer is a class, and the long functions are split one per step.
  - `python:S9073`: the acceptance job's two assertions are split.
- **Copilot**, 32 rounds. Every finding was real and was fixed with evidence, a case and a mutant; every thread is answered and resolved. Three overviews named a class with no finding: the line comments' (fixed), the identifiers' (fixed) and the URL resolution's (an accepted limit, see "Accepted limits").
  - At `1c064bb3`, fixed in `32ef3e8c`:
    - `r4170450448`: a module refused as a dynamic import hid a later static import of the same path.
    - `r4170450491`: a malformed 200 catalog raised a harness error instead of a named failure.
  - At `bbdeb9ec`: `r4170537382`, the same static-after-dynamic case. Already covered by `32ef3e8c`, and answered there.
  - At `32ef3e8c`, fixed in `b4fc637f`:
    - `r4170537350`: a server on `/tmp`'s socket alone, with TCP off, passed the fixed socket list.
    - `r4170567257`: a non-list `documents` value passed as "non-empty".
    - A "previously missed" item: a grouping value with no tile holding an id and a member document passed, and silently dropped the rail's thread read.
  - At `b4fc637f`, "Needs a closer look" with no new finding. It noted that the PR needs integrated verification with the dependent product changes: that is the integration above, now including T084. Its "previously missed" item, fixed in `b440d12b`: an `OSError` in `prepare()` escaped `main()` with exit 1. This head returns 2; at `b4fc637f`, an `ENOSPC` from `tempfile.mkdtemp` escaped.
  - At `b440d12b`, "Needs a closer look" with no findings. Its "previously missed" item, fixed in `f0e0ffe1`: the module-graph verdict had no collected regression test, and on this branch the acceptance job stops before `derive_bundle`. The new `tests/test_at_r1_http_harness.py` above answers it.
  - At `f0e0ffe1`, "Needs a closer look" with no findings. Its "previously missed" item, fixed in `4dcb4221`: a 200 catalog of `{"models": []}` passed, though `adoptCatalog` adopts only `schema_version === 1` and `kind === "workbench-model-catalog"`, and shows any other catalog as unreadable. The envelope is now a named check, and the served bundle must name the kind. Seven refused envelopes are tested, and relaxing the int check fails the `true` case.
  - At `4dcb4221`, "Needs a closer look" with no findings.
  - At `1c0ff975`, fixed in `f29b4ddd`: `r4173473346`, the envelope check refused `"schema_version": 1.0` and `1e0`, which JavaScript's `=== 1` adopts. It now admits any JSON number equal to 1, and never `true` or `"1"`.
  - At `f29b4ddd` ("Changes recommended"), fixed in `de8b2274`:
    - `r4173769822`: a module served 200 as `text/plain` or `text/html` passed;
    - `r4173769844`: so did a stylesheet.

    Both now require their type.
  - At `d53a7378` ("Changes recommended"), fixed in `5636eb8d`:
    - `r4173842763`: a forward holding a backslash or user information passed, though a browser opens another host;
    - `r4173842794`: a refused destination's message quoted a URL that could hold the token;
    - `r4173842805` and `r4173842811`: an unparseable printed location or refresh URL raised a harness error instead of a named failure.
  - At `5636eb8d` ("Changes recommended"), fixed in `486e426e`:
    - `r4173894317`: a refresh whose delay a browser aborts (`invalid;url=`, `-1;url=`) still yielded a token;
    - `r4173894352`: a token that `takeDeliveredConsoleToken` discards (`short`) passed.
  - At `486e426e`, "Needs a closer look" with no findings. Its "previously missed" item, fixed in `142d1352`: with an agreeing record, a forward to `/missing.html` or `/snapshot.json` passed. The forward must now open `/` or `/index.html`.
  - At `142d1352` ("Changes recommended"), fixed in `27479495`:
    - `r4174355680`: a percent-encoded copy of the token in the query passed;
    - previously missed: a forward to `[::1]` passed, though the plane listens on 127.0.0.1 alone;
    - previously missed: an import from `https://…` was fetched as its local path and passed;
    - previously missed: any stop exit status passed.
  - At `27479495` ("Changes recommended"), fixed in `64dc06f5`:
    - `r4174411680`: a bare specifier (`import "child.js"`) passed when `/child.js` was served;
    - previously missed: a FIFO opener hung the harness under `--keep-going`.
  - At `33841d4a` ("Changes recommended"), fixed in `4809b3d2`:
    - `r4174621486`: a start or route failure quoted the tail of the entry point's output, which may hold the token;
    - `r4174621535`: `documents: [{}]` passed, while every document's source reads were skipped.
  - At `4809b3d2` ("Changes recommended"), fixed in `32fbb6dc`:
    - `r4174671390`: a refresh inside `<template>` or `<noscript>` counted as the opener's forward. Only a live refresh counts now, as a browser that runs scripts parses the page;
    - `r4174671426`: the privacy check stopped at `console/`. The opener's whole path is now judged by T104's own rules, and `prepare()` refuses an unsafe `TMPDIR`;
    - and, from the holder's brief, no line the harness prints quotes a token (`Verdict.redact`).
  - At `ec95f451` ("Changes recommended"), fixed in `4d0e6d10`:
    - `r4175016672`: a token nested in `/capabilities` was echoed by the install-mode reason. No step-5 reason quotes a payload now, a published token is kept secret at any depth, and the opener's tokens are learned before step 5;
    - `r4175016692`: a body nested past the JSON parser's depth was a harness ERROR (exit 2). It is a named failure now.
  - At `82869769` ("Changes recommended"), fixed in `2dcb98d3`:
    - `r4177924060`: an opener past 64 KiB was judged by its truncated prefix. It is read whole or refused;
    - `r4177924097`: the catalog's reasons quoted its body, where a `\u`-escaped token passed the literal redaction. No reason quotes the server's bytes now;
    - `r4177924129`: same-origin was `http://loopback`, so an absolute same-origin import was refused. It is the running server's origin now.
  - At `2dcb98d3` ("Changes recommended"), fixed in `4bdb41fb`:
    - `r4178069345`: `json.loads` admitted `NaN` and `Infinity`, which `response.json()` refuses;
    - `r4178069374`: a backslash URL resolved to a local file, where a browser goes elsewhere;
    - and, on the holder's word, one adversarial pass over every parser, closing the class: JSON decoding, JavaScript escapes, the page's links, `<base>` and import maps, user information, encoded dot segments, request-target encoding, unterminated character references, and availability as the rail reads it.
  - At `4bdb41fb` ("Changes recommended"), fixed in `ba216f84`:
    - `r4178395669`: every `<script src>` was a module root, whatever its type, and inside a `<template>` too. Only a live module script is an entry now; a classic or inline script is refused by name, and a page with no entry fails;
    - `r4178395690`: `\uD83D\uDE00` left two surrogates that no codec encodes. Every string from JavaScript or JSON is read in Unicode scalar values now;
    - and the pass that closed each class: markup the harness does not model, non-UTF-8 pages, escapes a module refuses, redirects, `Content-Type` parsing, and printed lines a server could forge.
  - At `ba216f84` ("Changes recommended"), fixed in `9229c659`:
    - `r4178913887`: U+FEFF after `import` hid the import, as Python's `\s` lacks it. The lexer reads JavaScript's own whitespace now, and every run as one space, so no run or comment hides an import either;
    - `r4178913911`: the catalog's reasons quoted its envelope values and available ids, where a percent-encoded token passed the redaction. No reason quotes a value the product sent now, and the redaction sees through percent-encoding;
    - `r4178913926`: an encoded dot inside a file name (`child%2Ejs`) was refused as a dot segment. Only a whole segment is one now.
  - At `9229c659`, "Needs a closer look" with no findings. Its overview, fixed in `3392f934`: a line comment ended only at LF, so one ended by CR, U+2028 or U+2029 hid the next line's import. A hashbang line is read as a comment too.
  - At `3392f934` ("Changes recommended"), fixed in `0717f72f`:
    - `r4179115282`: a template's `${…}` was skipped by counting braces, so an import or a route inside one was never seen. It is read as code now;
    - `r4179115311`: only `/` was checked and walked, though the opener opens `/index.html`. The page the forward opens is asked of the forward's own host, with its query, must be HTML and UTF-8, and is the page whose bundle, routes and catalog are read.
  - At `0717f72f` ("Changes recommended"), fixed in `b7b9b843`:
    - `r4179220628`: a `${` left the lexer after an expression, so a `/` opening a regular expression read as division, and the expression's `}` closed the interpolation early. An interpolation starts an expression now;
    - `r4179220641`: `html.unescape` read `&copy=2` in a page's `src` or `href`, which a browser keeps as text. Such a tag is refused by name, as the opener's refresh already was.
  - At `b7b9b843` ("Changes recommended"), fixed in `d50e8cef`:
    - `r4179348386`: after a postfix `++` or `--` (`n++ / 2`), a `/` was read as a regular expression, which swallowed the next import. The lexer now reads what ends before a `/`: a postfix operator, a call or a group divides, and a prefix operator or the `)` of an `if`, `while`, `for` or `with` condition opens a regular expression;
    - `r4179348414`: an accepted `<base href="/">` was ignored, so `src="?m=1"` on `/index.html` was asked as `/index.html?m=1`, not `/?m=1`. The page's scripts and stylesheets resolve against its base URL now.
  - At `d50e8cef` ("Changes recommended"), fixed in `04579fcd`: `r4180454790`, a `/` after a `}` that ends an expression (`{} / 2`, a function or class expression) divides, and was read as a regular expression. A lexer cannot tell such a `}` from a block's, so a module with a `/` right after a `}` is refused by name now (`[bundle.ambiguous-slash <path>]`). The real bundle has none (the lexer decides 213 `/` there, none after a `}`). `ff04e015` then merged `main` `32943cbf` (T104).
  - At `ff04e015` ("Changes recommended"), fixed in `1d581541`: `r4180554323`, a member named as a keyword (`obj.return / 2`, `obj.if(1) / 2`, `obj.of++ / 2`) was read as the keyword, so each `/` hid the import after it. A member's name is never a keyword now. The self-pass after it, in `ea4f7838`: the last `.` of a spread (`[...typeof /re/]`) and a decimal literal's own point (`1. in /re/`) are no member access, so the keyword after either is still one; and the last name is read whole, so `ñreturn` and `this.#return` are names.
  - At `ea4f7838`, "Needs a closer look" with no findings. Its overview named "a verified lexer error" that "can hide missing static dependencies". The pass that followed, in `bd50f405` (with a case in `234229d7` for a mutant that survived), found four, each hiding an import: `of` read as a keyword outside a `for` head (`const of = 4; of / 2`); no condition opened by `for await (`; a regular expression after a spread's `...` read as a division; and `default`, `extends`, and `break`, `continue` and `debugger` before a line break, after which an expression or a statement starts.
  - At `234229d7` ("Changes recommended"), fixed in `09d9d632`: `r4180899047`, `of` was a keyword anywhere in a `for` head, so in a classic loop's initializer, condition or update (`let of = 4; for (; of / 2; ) break;`) the `/` opened a regular expression that swallowed the import after it. `of` is a keyword only as the head's separator now, right after its binding or target. The self-pass after it, in `e4f81d48`, is a differential check of the lexer against node's own module parser, run locally (the static module requests of a `vm.SourceTextModule`). It covers 196 contexts, each a regular expression or a division before a static import, with five kinds of separator. Node accepts 978 of the snippets as modules. The lexer hid the import in 195 of them at `ff04e015`, and in 85 at `09d9d632`. It now hides it in 10, all one form: a declaration list's last binding with no initializer, then a line break and a regular expression (`let x, y` [line break] `/re/`), which would take a parser. It has no false failure, and the same 10 are refused by name throughout. The 85 were a regular expression after a division's own `/`, and after a line break that ends a statement by automatic semicolon insertion: before a `++` or `--`, after a labelled `break` or `continue`, a declaration's lone binding, or a static import's specifier.
  - At `e4f81d48`, "Needs a closer look" with no findings. Its overview named "URL-resolution and identifier-parsing errors". Under the holder's convergence ruling (see "Accepted limits"), the identifier class was a one-line fix with a test, made in `d698c6ea`: a ZWNJ, a ZWJ, a combining mark or another name character that Python's `\w` lacks split a name. The URL class is an accepted limit: it appears in contrived references only, and all 84 shipped specifiers resolve as the URL standard resolves them.
  - At `d698c6ea`, "Needs a closer look" with no findings. The overview says the harness's "custom browser parsing and database-process lifecycle checks warrant final integrated human verification", and names no form. It was the last push of the review rounds.
  - `d29e68ca` merges `main` `dede32b4` for the T095 refresh. The merge is clean (main and this branch share no file) and changes nothing of this PR's own. At `d29e68ca`: "Needs a closer look" with no findings and no thread. Its overview repeats `d698c6ea`'s: the browser modeling and the database-process lifecycle checks "warrant human sign-off despite reported green CI". It names no form, so there is nothing to classify.

## Plan changes this PR carries

- **T007 batch N** (openxFactory `bdd0f586`, RULED `5963851934`): the raw `/capabilities` payload carries the console token neither by name, at any depth, nor by value, as quickstart § 3 asserts it; `console/` is 0700 exactly (`eb81b3d8`). The id CI's by-design failure names, `[a.capabilities carries no console token]`, is the by-name check. Product mutant m12 (the token under another key) is killed by the by-value check.
- **The holder's T096 dry run, F4, after openDox-code#85** (the T102 follow-on, holder ruling F1 (i)): a standalone plane's rail sends no thread read, so the harness asks none with a query, and asserts the condition, no branch-session column (`4d0e6d10`).

## Not done here

- No ruleset change: making `acceptance` required is the owner's.
- No fix to any other writer's PR; gaps found are reported.
- No README edit: the openDox root's README is T076's (openDox#17).

🤖 Generated with [Claude Code](https://claude.com/claude-code)


Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants